rpgm_asset_decrypter_lib/lib.rs
1#![warn(clippy::all, clippy::pedantic)]
2#![allow(clippy::needless_doctest_main)]
3#![allow(clippy::cast_possible_truncation)]
4#![allow(clippy::cast_possible_wrap)]
5#![allow(clippy::cast_sign_loss)]
6#![allow(clippy::deref_addrof)]
7
8/*!
9# rpgm-asset-decrypter-lib
10
11**BLAZINGLY** :fire: fast and tiny library for decrypting RPG Maker MV/MZ `rpgmvp`/`png_`, `rpgmvo`/`ogg_`, `rpgmvm`/`m4a_` assets.
12
13This project essentially is a rewrite of Petschko's [RPG-Maker-MV-Decrypter](https://gitlab.com/Petschko/RPG-Maker-MV-Decrypter) in Rust, but it also implements encryption key extraction from non-image files, such as `rpgmvo`/`ogg_` (with `vorbis-key-extraction` feature) and `rpgmvm`/`m4a_`.
14
15And since it's implemented in Rust 🦀🦀🦀, it's also very tiny, clean, and performant.
16
17Used in my [rpgm-asset-decrypter-rs](https://github.com/savannstm/rpgm-asset-decrypter-rs) CLI tool.
18
19## Installation
20
21`cargo add rpgm-asset-decrypter-lib`
22
23## Usage
24
25Decrypt:
26
27```no_run
28use rpgm_asset_decrypter_lib::{Decrypter, FileType};
29use std::fs::{read, write};
30
31let mut decrypter = Decrypter::new();
32let file = "./picture.rpgmvp";
33
34let buf = read(file).unwrap();
35
36// Decrypter auto-determines the encryption key from data, but you also need to pass a file type.
37let decrypted = decrypter.decrypt(&buf, FileType::PNG).unwrap();
38
39// You can also auto-deduce FileType from extension:
40// FileType::from("rpgmvp");
41// It supports conversions from &str and &OsStr.
42
43// Since [`Decrypter::decrypt`] copies the data, it's pretty much inefficient, and if you don't need to reuse the file data, you can decrypt it in-place:
44// let mut buf = read(file).unwrap();
45// [`Decrypter::decrypt_in_place`] returns a slice of the actual decrypted data, so use that.
46// let sliced = decrypter.decrypt_in_place(&mut buf, FileType::PNG);
47// write("./decrypted-picture.png", sliced).unwrap();
48
49write("./decrypted-picture.png", decrypted).unwrap();
50```
51
52Encrypt:
53
54```no_run
55use rpgm_asset_decrypter_lib::{Decrypter, DEFAULT_KEY};
56use std::fs::{read, write};
57
58let mut decrypter = Decrypter::new();
59
60let file = "./picture.png";
61let buf = read(file).unwrap();
62
63// When encrypting, decrypter requires a key.
64// You can grab the key from System.json file or use [`Decrypter::set_key_from_file`] with RPG Maker encrypted file content.
65//
66// let encrypted = read("./picture.rpgmvp").unwrap();
67// decrypter.set_key_from_file(&encrypted, FileType::PNG);
68//
69// You can also use default key:
70// decrypter.set_key_from_str(DEFAULT_KEY);
71// but I don't recommend using that.
72let encrypted = decrypter.encrypt(&buf).unwrap();
73
74// You can also auto-deduce FileType from extension:
75// FileType::from("rpgmvp");
76// It supports conversions from &str and &OsStr.
77
78// Since [`Decrypter::encrypt`] copies the data, it's pretty much inefficient, and if you don't need to reuse the file data, you can encrypt it in-place:
79// let mut buf = read(file).unwrap();
80// [`Decrypter::decrypt_in_place`] doesn't include the RPG Maker header in encrypted data,
81// but we can write everything into a file more efficient with vectored I/O.
82// use rpgm_asset_decrypter_lib::{RPGM_HEADER};
83// use std::fs::File;
84// use std::io::{self, Write, IoSlice};
85// decrypter.encrypt_in_place(&mut buf);
86// let mut file = File::create("./encrypted-picture.rpgmvp");
87// let bufs = [IoSlice::new(RPGM_HEADER), IoSlice::new(buf)];
88// file.write_vectored(&bufs).unwrap();
89
90write("./encrypted-picture.rpgmvp", encrypted).unwrap();
91```
92
93## Features
94
95- `serde` - enables serde serialization/deserialization for `Error` type.
96- `vorbis-key-extraction` - enables key auto-extraction from `rpgmvo`/`ogg_` OGG files. This is made as a feature because it depends on `vorbis-rs` which pulls a handful of libraries.
97
98## License
99
100Project is licensed under WTFPL.
101*/
102
103use std::ffi::OsStr;
104#[cfg(feature = "vorbis-key-extraction")]
105use std::io::Cursor;
106use thiserror::Error;
107#[cfg(feature = "vorbis-key-extraction")]
108use vorbis_rs::VorbisDecoder;
109
110const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
111
112pub const HEADER_LENGTH: usize = 16;
113
114pub const KEY_LENGTH: usize = 16;
115pub const KEY_STR_LENGTH: usize = 32;
116
117// Key used in RPG Maker encrypted files when "Encryption key" is left unfilled.
118pub const DEFAULT_KEY: &str = "d41d8cd98f00b204e9800998ecf8427e";
119
120// RPG Maker's encoding is essentially taking source file's header (16 bytes) and xor'ing it upon a MD5 key produced from encryption key string. Most projects leave encryption key string empty, so resulting 'encryption' is just header xor'd with default MD5 key.
121
122// For PNG, header is always the same, so we can expect valid decryption.
123const PNG_HEADER: [u8; HEADER_LENGTH] = [
124 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
125 0x49, 0x48, 0x44, 0x52,
126];
127
128// 0 - 3 - OggS
129// 4 - version, always 0
130// 5 - header type, always 0x02, since first page always announces the beginning of the stream
131// 6 - 13 - granule position, always 0, since first page has no actual data
132//* 14 - 15 - part of 4-byte bitstream serial number, that actually differs between files
133const OGG_HEADER: [u8; HEADER_LENGTH] =
134 [79, 103, 103, 83, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
135
136//* 0 - 3 - type box size, actually differs between files
137// 4 - 7 - ftyp, always the same
138// 8 - 11 - M4A_, always the same, may be different 4 characters, but extremely unlikely
139// 12 - 15 - minor version, mostly junk, doesn't matter
140static mut M4A_HEADER: [u8; HEADER_LENGTH] =
141 [0, 0, 0, 28, 102, 116, 121, 112, 77, 52, 65, 32, 0, 0, 2, 0];
142
143// For finding type box size
144const M4A_POST_HEADER_BOXES: &[&[u8]] =
145 &[b"moov", b"mdat", b"free", b"skip", b"wide", b"pnot"];
146
147// Every encrypted file includes this header.
148pub const RPGM_HEADER: [u8; HEADER_LENGTH] = [
149 0x52, 0x50, 0x47, 0x4d, 0x56, 0x00, 0x00, 0x00, 0x00, 0x03, 0x01, 0x00,
150 0x00, 0x00, 0x00, 0x00,
151];
152
153#[derive(PartialEq, Clone, Copy)]
154#[repr(u8)]
155pub enum FileType {
156 PNG,
157 OGG,
158 M4A,
159}
160
161impl From<&str> for FileType {
162 fn from(value: &str) -> Self {
163 match value {
164 "rpgmvp" | "png_" => FileType::PNG,
165 "rpgmvo" | "ogg_" => FileType::OGG,
166 "rpgmvm" | "m4a_" => FileType::M4A,
167 _ => panic!(),
168 }
169 }
170}
171
172// [`PathBuf::extension`] returns &OsStr, so implement this for convenience.
173impl From<&OsStr> for FileType {
174 fn from(value: &OsStr) -> Self {
175 if value == "rpgmvp" || value == "png_" {
176 FileType::PNG
177 } else if value == "rpgmvo" || value == "ogg_" {
178 FileType::OGG
179 } else if value == "rpgmvm" || value == "m4a_" {
180 FileType::M4A
181 } else {
182 panic!()
183 }
184 }
185}
186
187#[derive(Debug, Error)]
188#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
189pub enum Error {
190 #[error(
191 "Key must be set using any of `set_key` methods before calling `encrypt` function."
192 )]
193 KeyNotSet,
194 #[error("Key must have a fixed length of 32 characters.")]
195 InvalidKeyLength,
196 #[error(
197 "Passed data has invalid header. RPG Maker encrypted files should always start with RPGMV header. Either passed data is not RPG Maker data or it's corrupted."
198 )]
199 InvalidHeader,
200 #[error(
201 "Unexpected end of file encountered. Either passed data is not RPG Maker data or it's corrupted."
202 )]
203 UnexpectedEOF,
204 #[error(
205 "Vorbis (`rpgmvo`/`ogg_`) key extraction is not supported, since `vorbis-key-extraction` feature is disabled. If you're a developer, enable this feature or indicate users that they need to extract the key from a different file."
206 )]
207 VorbisKeyExtractionUnsupported,
208}
209
210#[derive(Default)]
211pub struct Decrypter {
212 key_hex: [u8; KEY_STR_LENGTH],
213 key: [u8; KEY_LENGTH],
214 has_key: bool,
215}
216
217impl Decrypter {
218 /// Creates a new Decrypter instance.
219 ///
220 /// Decrypter requires a key, which you can set from [`Decrypter::set_key_from_str`] and [`Decrypter::set_key_from_file`] functions.
221 /// You can get the key string from `encryptionKey` field in `System.json` file or from any encrypted RPG Maker file.
222 ///
223 /// [`Decrypter::decrypt`] function will automatically determine the key from the input file, so you usually don't need to set it manually.
224 #[must_use]
225 pub fn new() -> Self {
226 Self::default()
227 }
228
229 #[inline]
230 /// Converts human-readable hex to the real key bytes.
231 fn set_key_from_hex(&mut self) {
232 for (j, i) in (0..self.key_hex.len()).step_by(2).enumerate() {
233 let u8_hex = [self.key_hex[i], self.key_hex[i + 1]];
234 let u8_hex_str = unsafe { std::str::from_utf8_unchecked(&u8_hex) };
235 self.key[j] = u8::from_str_radix(u8_hex_str, 16).unwrap();
236 }
237
238 self.has_key = true;
239 }
240
241 #[inline]
242 /// Either decrypts or encrypts the passed buffer, depending on the place this function was invoked from.
243 ///
244 /// Actual encryption is just: xor buffer's first 16 bytes with key.
245 fn xor_buffer(&self, buffer: &mut [u8]) {
246 for (i, item) in buffer.iter_mut().enumerate().take(HEADER_LENGTH) {
247 *item ^= self.key[i];
248 }
249 }
250
251 /// Returns the decrypter's key, or [`None`] if it's not set.
252 #[inline]
253 #[must_use]
254 pub fn key(&self) -> Option<&str> {
255 if !self.has_key {
256 return None;
257 }
258
259 Some(unsafe { std::str::from_utf8_unchecked(&self.key_hex) })
260 }
261
262 /// Sets the decrypter's key to provided `&str` hex string.
263 ///
264 /// # Returns
265 ///
266 /// If key's length is not 32 bytes, the function fails and returns [`Error`].
267 ///
268 /// # Errors
269 ///
270 /// - [`Error::InvalidKeyLength`] - if key's length is not 32 bytes.
271 #[inline]
272 pub fn set_key_from_str(&mut self, key: &str) -> Result<(), Error> {
273 if key.len() != KEY_STR_LENGTH {
274 return Err(Error::InvalidKeyLength);
275 }
276
277 self.key_hex =
278 unsafe { *key.as_bytes().as_ptr().cast::<[u8; KEY_STR_LENGTH]>() };
279 self.set_key_from_hex();
280
281 Ok(())
282 }
283
284 /// Sets the key of decrypter from encrypted `file_content` data.
285 ///
286 /// # Arguments
287 ///
288 /// - `file_content` - The data of RPG Maker file.
289 ///
290 /// # Returns
291 ///
292 /// - Reference to the key string, if succeeded.
293 /// - [`Error`] otherwise.
294 ///
295 /// # Errors
296 ///
297 /// - [`Error::InvalidHeader`] - if passed `file_content` data contains invalid header.
298 /// - [`Error::UnexpectedEOF`] - if passed `file_content` data ends unexpectedly.
299 /// - [`Error::VorbisKeyExtractionUnsupported`] - if passed `file_type` is [`FileType::OGG`] and `vorbis-key-extraction` feature is disabled.
300 #[inline]
301 pub fn set_key_from_file(
302 &mut self,
303 file_content: &[u8],
304 file_type: FileType,
305 ) -> Result<&str, Error> {
306 #[cfg(not(feature = "vorbis-key-extraction"))]
307 if file_type == FileType::OGG {
308 return Err(Error::VorbisKeyExtractionUnsupported);
309 }
310
311 if !file_content.starts_with(&RPGM_HEADER) {
312 return Err(Error::InvalidHeader);
313 }
314
315 let Some(post_header) =
316 file_content.get(HEADER_LENGTH..HEADER_LENGTH * 2)
317 else {
318 return Err(Error::UnexpectedEOF);
319 };
320
321 // Get proper M4A header box size
322 //* We don't care about anything else for M4A, since `ftypM4A_` in M4A header can be easily replaced by `ftypSHIT`, and FFmpeg will have ZERO complains.
323 //* The same goes for 12-15 bytes (inclusive), they can be overwritten with whatever integer.
324 if file_type == FileType::M4A {
325 let header_64_bytes =
326 &file_content[HEADER_LENGTH..HEADER_LENGTH + 64];
327 let header_chunks = header_64_bytes.chunks_exact(4);
328
329 for (i, chunk) in header_chunks.enumerate() {
330 if M4A_POST_HEADER_BOXES.contains(&chunk) {
331 unsafe {
332 M4A_HEADER[..4].copy_from_slice(
333 &(((i - 1) * 4) as u32).to_le_bytes(),
334 );
335 }
336 }
337 }
338 }
339
340 let mut j = 0;
341 for i in 0..HEADER_LENGTH {
342 let signature_byte = match file_type {
343 FileType::PNG => PNG_HEADER[i],
344 FileType::OGG => OGG_HEADER[i],
345 FileType::M4A => unsafe { M4A_HEADER[i] },
346 };
347
348 let value = signature_byte ^ post_header[i];
349
350 let high = HEX_CHARS[(value >> 4) as usize];
351 let low = HEX_CHARS[(value & 0x0F) as usize];
352
353 self.key_hex[j] = high;
354 self.key_hex[j + 1] = low;
355 j += 2;
356 }
357
358 #[cfg(feature = "vorbis-key-extraction")]
359 if file_type != FileType::OGG
360 || VorbisDecoder::new(Cursor::new(&file_content[HEADER_LENGTH..]))
361 .is_ok()
362 {
363 self.set_key_from_hex();
364 return Ok(unsafe { std::str::from_utf8_unchecked(&self.key_hex) });
365 }
366
367 #[cfg(not(feature = "vorbis-key-extraction"))]
368 {
369 self.set_key_from_hex();
370 Ok(unsafe { std::str::from_utf8_unchecked(&self.key_hex) })
371 }
372
373 #[cfg(feature = "vorbis-key-extraction")]
374 // Brute-forcing 14-15th OGG header bytes
375 {
376 let mut candidate = file_content[HEADER_LENGTH..].to_vec();
377 let mut candidate_backup: [u8; 16] = Default::default();
378 candidate_backup[..16].copy_from_slice(&candidate[..16]);
379
380 for b15 in 0..=u8::MAX {
381 for b16 in 0..=u8::MAX {
382 candidate[..16].copy_from_slice(&candidate_backup[..16]);
383
384 self.key_hex[28] = HEX_CHARS[(b15 >> 4) as usize];
385 self.key_hex[29] = HEX_CHARS[(b15 & 0x0F) as usize];
386 self.key_hex[30] = HEX_CHARS[(b16 >> 4) as usize];
387 self.key_hex[31] = HEX_CHARS[(b16 & 0x0F) as usize];
388
389 self.set_key_from_hex();
390 self.xor_buffer(&mut candidate);
391
392 if VorbisDecoder::new(Cursor::new(&candidate)).is_ok() {
393 return Ok(unsafe {
394 std::str::from_utf8_unchecked(&self.key_hex)
395 });
396 }
397 }
398 }
399
400 unreachable!(
401 "We should've handled OGG files, but something has gone wrong."
402 );
403 }
404 }
405
406 /// Decrypts RPG Maker file content.
407 /// Auto-determines the key from the input file.
408 ///
409 /// This function copies the contents of the file and returns decrypted [`Vec<u8>`] copy.
410 /// If you want to avoid copying, see [`Decrypter::decrypt_in_place`] function.
411 ///
412 /// # Arguments
413 ///
414 /// - `file_content` - The data of RPG Maker file.
415 /// - `file_type` - [`FileType`], representing whether passed file content is PNG, OGG or M4A.
416 ///
417 /// # Returns
418 ///
419 /// - [`Error`], if passed `file_content` data has invalid header.
420 /// - [`Vec<u8>`] containing decrypted data otherwise.
421 ///
422 /// # Errors
423 ///
424 /// - [`Error::InvalidHeader`] - if passed `file_content` data has invalid header.
425 /// - [`Error::UnexpectedEOF`] - if passed `file_content` data ends unexpectedly.
426 #[inline]
427 pub fn decrypt(
428 &mut self,
429 file_content: &[u8],
430 file_type: FileType,
431 ) -> Result<Vec<u8>, Error> {
432 if !file_content.starts_with(&RPGM_HEADER) {
433 return Err(Error::InvalidHeader);
434 }
435
436 if !self.has_key {
437 self.set_key_from_file(file_content, file_type)?;
438 }
439
440 let mut result = file_content[HEADER_LENGTH..].to_vec();
441 self.xor_buffer(&mut result);
442 Ok(result)
443 }
444
445 /// Decrypts RPG Maker file content.
446 /// Auto-determines the key from the input file.
447 ///
448 /// This function decrypts the passed file data in-place.
449 /// If you don't want to modify passed data, see [`Decrypter::decrypt`] function.
450 ///
451 /// # Note
452 ///
453 /// Decrypted data is only valid starting at offset 16. This function returns the reference to the correct slice.
454 ///
455 /// # Arguments
456 ///
457 /// - `file_content` - The data of RPG Maker file.
458 /// - `file_type` - [`FileType`], representing whether passed file content is PNG, OGG or M4A.
459 ///
460 /// # Returns
461 ///
462 /// - [`Error`], if passed `file_content` data has invalid header.
463 /// - Reference to a slice of the passed `file_content` data starting at offset 16 otherwise.
464 ///
465 /// # Errors
466 ///
467 /// - [`Error::InvalidHeader`] - if passed `file_content` data has invalid header.
468 /// - [`Error::UnexpectedEOF`] - if passed `file_content` data ends unexpectedly.
469 #[inline]
470 pub fn decrypt_in_place<'a>(
471 &'a mut self,
472 file_content: &'a mut [u8],
473 file_type: FileType,
474 ) -> Result<&'a [u8], Error> {
475 if !file_content.starts_with(&RPGM_HEADER) {
476 return Err(Error::InvalidHeader);
477 }
478
479 if !self.has_key {
480 self.set_key_from_file(file_content, file_type)?;
481 }
482
483 let sliced_past_header = &mut file_content[HEADER_LENGTH..];
484 self.xor_buffer(sliced_past_header);
485 Ok(sliced_past_header)
486 }
487
488 /// Encrypts file content.
489 ///
490 /// This function requires decrypter to have a key, which you can fetch from `System.json` file
491 /// or by calling [`Decrypter::set_key_from_file`] with the data from encrypted file.
492 ///
493 /// This function copies the contents of the file and returns encrypted [`Vec<u8>`] copy.
494 /// If you want to avoid copying, see [`Decrypter::encrypt_in_place`] function.
495 ///
496 /// # Arguments
497 ///
498 /// - `file_content` - The data of `.png`, `.ogg` or `.m4a` file.
499 ///
500 /// # Returns
501 ///
502 /// - [`Vec<u8>`] containing encrypted data if decrypter key is set.
503 /// - [`Error`] otherwise.
504 ///
505 /// # Errors
506 ///
507 /// - [`Error::KeyNotSet`] - if decrypter's key is not set.
508 #[inline]
509 pub fn encrypt(&self, file_content: &[u8]) -> Result<Vec<u8>, Error> {
510 if !self.has_key {
511 return Err(Error::KeyNotSet);
512 }
513
514 let mut data = file_content.to_vec();
515 self.xor_buffer(&mut data);
516
517 let mut output_data = Vec::with_capacity(HEADER_LENGTH + data.len());
518 output_data.extend(RPGM_HEADER);
519 output_data.extend(data);
520 Ok(output_data)
521 }
522
523 /// Encrypts file content in-place.
524 ///
525 /// This function requires decrypter to have a key, which you can fetch from `System.json` file
526 /// or by calling [`Decrypter::set_key_from_file`] with the data from encrypted file.
527 ///
528 /// This function encrypts the passed file data in-place.
529 /// If you don't want to modify passed data, see [`Decrypter::encrypt`] function.
530 ///
531 /// # Note
532 ///
533 /// Encrypted data comes without the RPG Maker header, so you need to manually prepend it - but you can decide where and how to do it most efficient.
534 /// The header is exported as [`RPGM_HEADER`].
535 ///
536 /// # Arguments
537 ///
538 /// - `file_content` - The data of `.png`, `.ogg` or `.m4a` file.
539 ///
540 /// # Returns
541 ///
542 /// - Nothing, if decrypter key is set.
543 /// - [`Error`] otherwise.
544 ///
545 /// # Errors
546 ///
547 /// - [`Error::KeyNotSet`] - if decrypter's key is not set.
548 #[inline]
549 pub fn encrypt_in_place(
550 &self,
551 file_content: &mut [u8],
552 ) -> Result<(), Error> {
553 if !self.has_key {
554 return Err(Error::KeyNotSet);
555 }
556
557 self.xor_buffer(file_content);
558 Ok(())
559 }
560}