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_` 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
97## License
98
99Project is licensed under WTFPL.
100*/
101
102use std::{
103 ffi::OsStr,
104 io::{Cursor, Read, Seek, SeekFrom},
105};
106use thiserror::Error;
107
108const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
109
110pub const HEADER_LENGTH: usize = 16;
111
112pub const KEY_LENGTH: usize = 16;
113pub const KEY_STR_LENGTH: usize = 32;
114
115// Key used in RPG Maker encrypted files when "Encryption key" is left unfilled.
116pub const DEFAULT_KEY: &str = "d41d8cd98f00b204e9800998ecf8427e";
117
118// 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.
119
120// For PNG, header is always the same, so we can expect valid decryption.
121const PNG_HEADER: [u8; HEADER_LENGTH] = [
122 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
123 0x49, 0x48, 0x44, 0x52,
124];
125
126// 0 - 3 - OggS
127// 4 - version, always 0
128// 5 - header type, always 0x02, since first page always announces the beginning of the stream
129// 6 - 13 - granule position, always 0, since first page has no actual data
130//* 14 - 15 - part of 4-byte bitstream serial number, that actually differs between files
131static mut OGG_HEADER: [u8; HEADER_LENGTH] =
132 [79, 103, 103, 83, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
133
134//* 0 - 3 - type box size, actually differs between files
135// 4 - 7 - ftyp, always the same
136// 8 - 11 - M4A_, always the same, may be different 4 characters, but extremely unlikely
137// 12 - 15 - minor version, mostly junk, doesn't matter
138static mut M4A_HEADER: [u8; HEADER_LENGTH] =
139 [0, 0, 0, 28, 102, 116, 121, 112, 77, 52, 65, 32, 0, 0, 2, 0];
140
141// For finding type box size
142const M4A_POST_HEADER_BOXES: &[&[u8]] =
143 &[b"moov", b"mdat", b"free", b"skip", b"wide", b"pnot"];
144
145// Every encrypted file includes this header.
146pub const RPGM_HEADER: [u8; HEADER_LENGTH] = [
147 0x52, 0x50, 0x47, 0x4d, 0x56, 0x00, 0x00, 0x00, 0x00, 0x03, 0x01, 0x00,
148 0x00, 0x00, 0x00, 0x00,
149];
150
151#[derive(PartialEq, Clone, Copy)]
152#[repr(u8)]
153pub enum FileType {
154 PNG,
155 OGG,
156 M4A,
157}
158
159impl From<&str> for FileType {
160 fn from(value: &str) -> Self {
161 match value {
162 "rpgmvp" | "png_" => FileType::PNG,
163 "rpgmvo" | "ogg_" => FileType::OGG,
164 "rpgmvm" | "m4a_" => FileType::M4A,
165 _ => panic!(),
166 }
167 }
168}
169
170// [`PathBuf::extension`] returns &OsStr, so implement this for convenience.
171impl From<&OsStr> for FileType {
172 fn from(value: &OsStr) -> Self {
173 if value == "rpgmvp" || value == "png_" {
174 FileType::PNG
175 } else if value == "rpgmvo" || value == "ogg_" {
176 FileType::OGG
177 } else if value == "rpgmvm" || value == "m4a_" {
178 FileType::M4A
179 } else {
180 panic!()
181 }
182 }
183}
184
185#[derive(Debug, Error)]
186#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
187pub enum Error {
188 #[error(
189 "Key must be set using any of `set_key` methods before calling `encrypt` function."
190 )]
191 KeyNotSet,
192 #[error("Key must have a fixed length of 32 characters.")]
193 InvalidKeyLength,
194 #[error(
195 "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."
196 )]
197 InvalidHeader,
198 #[error(
199 "Unexpected end of file encountered. Either passed data is not RPG Maker data or it's corrupted."
200 )]
201 UnexpectedEOF,
202}
203
204#[derive(Default)]
205pub struct Decrypter {
206 key_hex: [u8; KEY_STR_LENGTH],
207 key: [u8; KEY_LENGTH],
208 has_key: bool,
209}
210
211impl Decrypter {
212 /// Creates a new Decrypter instance.
213 ///
214 /// Decrypter requires a key, which you can set from [`Decrypter::set_key_from_str`] and [`Decrypter::set_key_from_file`] functions.
215 /// You can get the key string from `encryptionKey` field in `System.json` file or from any encrypted RPG Maker file.
216 ///
217 /// [`Decrypter::decrypt`] function will automatically determine the key from the input file, so you usually don't need to set it manually.
218 #[must_use]
219 pub fn new() -> Self {
220 Self::default()
221 }
222
223 #[inline]
224 /// Converts human-readable hex to the real key bytes.
225 fn set_key_from_hex(&mut self) {
226 for (j, i) in (0..self.key_hex.len()).step_by(2).enumerate() {
227 let u8_hex = [self.key_hex[i], self.key_hex[i + 1]];
228 let u8_hex_str = unsafe { std::str::from_utf8_unchecked(&u8_hex) };
229 self.key[j] = u8::from_str_radix(u8_hex_str, 16).unwrap();
230 }
231
232 self.has_key = true;
233 }
234
235 #[inline]
236 /// Either decrypts or encrypts the passed buffer, depending on the place this function was invoked from.
237 ///
238 /// Actual encryption is just: xor buffer's first 16 bytes with key.
239 fn xor_buffer(&self, buffer: &mut [u8]) {
240 for (i, item) in buffer.iter_mut().enumerate().take(HEADER_LENGTH) {
241 *item ^= self.key[i];
242 }
243 }
244
245 fn read_ogg_page_serialno(file_content: &mut Cursor<&[u8]>) -> u32 {
246 let mut header: [u8; 27] = [0; 27];
247
248 file_content.read_exact(&mut header).unwrap();
249
250 let segment_count: usize = header[26] as usize;
251 let mut segment_table: [u8; u8::MAX as usize] = [0; u8::MAX as usize];
252
253 file_content.read_exact(&mut segment_table).unwrap();
254
255 let over_count = i64::from(u8::MAX) - segment_count as i64;
256
257 file_content.seek(SeekFrom::Current(-over_count)).unwrap();
258
259 let mut body_length: i64 = 0;
260
261 for segment in segment_table.iter().take(segment_count) {
262 body_length += i64::from(*segment);
263 }
264
265 file_content.seek(SeekFrom::Current(body_length)).unwrap();
266
267 let header_serialno =
268 unsafe { *header[14..18].as_ptr().cast::<[u8; 4]>() };
269
270 u32::from(header_serialno[0])
271 | (u32::from(header_serialno[1]) << 8)
272 | (u32::from(header_serialno[2]) << 16)
273 | (u32::from(header_serialno[3]) << 24)
274 }
275
276 /// Returns the decrypter's key, or [`None`] if it's not set.
277 #[inline]
278 #[must_use]
279 pub fn key(&self) -> Option<&str> {
280 if !self.has_key {
281 return None;
282 }
283
284 Some(unsafe { std::str::from_utf8_unchecked(&self.key_hex) })
285 }
286
287 /// Sets the decrypter's key to provided `&str` hex string.
288 ///
289 /// # Returns
290 ///
291 /// If key's length is not 32 bytes, the function fails and returns [`Error`].
292 ///
293 /// # Errors
294 ///
295 /// - [`Error::InvalidKeyLength`] - if key's length is not 32 bytes.
296 #[inline]
297 pub fn set_key_from_str(&mut self, key: &str) -> Result<(), Error> {
298 if key.len() != KEY_STR_LENGTH {
299 return Err(Error::InvalidKeyLength);
300 }
301
302 self.key_hex =
303 unsafe { *key.as_bytes().as_ptr().cast::<[u8; KEY_STR_LENGTH]>() };
304 self.set_key_from_hex();
305
306 Ok(())
307 }
308
309 /// Sets the key of decrypter from encrypted `file_content` data.
310 ///
311 /// # Arguments
312 ///
313 /// - `file_content` - The data of RPG Maker file.
314 ///
315 /// # Returns
316 ///
317 /// - Reference to the key string, if succeeded.
318 /// - [`Error`] otherwise.
319 ///
320 /// # Errors
321 ///
322 /// - [`Error::InvalidHeader`] - if passed `file_content` data contains invalid header.
323 /// - [`Error::UnexpectedEOF`] - if passed `file_content` data ends unexpectedly.
324 #[inline]
325 pub fn set_key_from_file(
326 &mut self,
327 file_content: &[u8],
328 file_type: FileType,
329 ) -> Result<&str, Error> {
330 if !file_content.starts_with(&RPGM_HEADER) {
331 return Err(Error::InvalidHeader);
332 }
333
334 let Some(post_header) =
335 file_content.get(HEADER_LENGTH..HEADER_LENGTH * 2)
336 else {
337 return Err(Error::UnexpectedEOF);
338 };
339
340 // Get proper M4A header box size
341 //* 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.
342 //* The same goes for 12-15 bytes (inclusive), they can be overwritten with whatever integer.
343 if file_type == FileType::M4A {
344 const CHUNK_SIZE: usize = 4;
345
346 let Some(file_start) =
347 file_content.get(HEADER_LENGTH..HEADER_LENGTH + 64)
348 else {
349 return Err(Error::UnexpectedEOF);
350 };
351
352 let file_start_chunks = file_start.chunks_exact(CHUNK_SIZE);
353
354 for (i, chunk) in file_start_chunks.enumerate() {
355 if M4A_POST_HEADER_BOXES.contains(&chunk) {
356 let prev_chunk_i = i - 1;
357 let header_type_box_size =
358 (prev_chunk_i * CHUNK_SIZE) as u32;
359
360 unsafe {
361 M4A_HEADER[..CHUNK_SIZE].copy_from_slice(
362 &header_type_box_size.to_le_bytes(),
363 );
364 }
365 }
366 }
367 }
368
369 // Since stream serial number is incorrect in OGG_HEADER because it's different for each file, we need to seek to the second page of the stream and grab the serial number from there, and then replace it in the header.
370 // Serial number is persistent across all pages of the stream, so we can gan grab it from the second page and replace in the first.
371 if file_type == FileType::OGG {
372 let mut file_content_cursor =
373 Cursor::new(&file_content[HEADER_LENGTH..]);
374
375 Decrypter::read_ogg_page_serialno(&mut file_content_cursor);
376
377 let serialno =
378 Decrypter::read_ogg_page_serialno(&mut file_content_cursor);
379
380 unsafe {
381 OGG_HEADER[14..16]
382 .clone_from_slice(&serialno.to_le_bytes()[0..2]);
383 }
384 }
385
386 let mut j = 0;
387 for i in 0..HEADER_LENGTH {
388 let signature_byte = match file_type {
389 FileType::PNG => PNG_HEADER[i],
390 FileType::OGG => unsafe { OGG_HEADER[i] },
391 FileType::M4A => unsafe { M4A_HEADER[i] },
392 };
393
394 let value = signature_byte ^ post_header[i];
395
396 let high = HEX_CHARS[(value >> 4) as usize];
397 let low = HEX_CHARS[(value & 0x0F) as usize];
398
399 self.key_hex[j] = high;
400 self.key_hex[j + 1] = low;
401 j += 2;
402 }
403
404 self.set_key_from_hex();
405 Ok(unsafe { std::str::from_utf8_unchecked(&self.key_hex) })
406 }
407
408 /// Decrypts RPG Maker file content.
409 /// Auto-determines the key from the input file.
410 ///
411 /// This function copies the contents of the file and returns decrypted [`Vec<u8>`] copy.
412 /// If you want to avoid copying, see [`Decrypter::decrypt_in_place`] function.
413 ///
414 /// # Arguments
415 ///
416 /// - `file_content` - The data of RPG Maker file.
417 /// - `file_type` - [`FileType`], representing whether passed file content is PNG, OGG or M4A.
418 ///
419 /// # Returns
420 ///
421 /// - [`Error`], if passed `file_content` data has invalid header.
422 /// - [`Vec<u8>`] containing decrypted data otherwise.
423 ///
424 /// # Errors
425 ///
426 /// - [`Error::InvalidHeader`] - if passed `file_content` data has invalid header.
427 /// - [`Error::UnexpectedEOF`] - if passed `file_content` data ends unexpectedly.
428 #[inline]
429 pub fn decrypt(
430 &mut self,
431 file_content: &[u8],
432 file_type: FileType,
433 ) -> Result<Vec<u8>, Error> {
434 if !file_content.starts_with(&RPGM_HEADER) {
435 return Err(Error::InvalidHeader);
436 }
437
438 if !self.has_key {
439 self.set_key_from_file(file_content, file_type)?;
440 }
441
442 let mut result = file_content[HEADER_LENGTH..].to_vec();
443 self.xor_buffer(&mut result);
444 Ok(result)
445 }
446
447 /// Decrypts RPG Maker file content.
448 /// Auto-determines the key from the input file.
449 ///
450 /// This function decrypts the passed file data in-place.
451 /// If you don't want to modify passed data, see [`Decrypter::decrypt`] function.
452 ///
453 /// # Note
454 ///
455 /// Decrypted data is only valid starting at offset 16. This function returns the reference to the correct slice.
456 ///
457 /// # Arguments
458 ///
459 /// - `file_content` - The data of RPG Maker file.
460 /// - `file_type` - [`FileType`], representing whether passed file content is PNG, OGG or M4A.
461 ///
462 /// # Returns
463 ///
464 /// - [`Error`], if passed `file_content` data has invalid header.
465 /// - Reference to a slice of the passed `file_content` data starting at offset 16 otherwise.
466 ///
467 /// # Errors
468 ///
469 /// - [`Error::InvalidHeader`] - if passed `file_content` data has invalid header.
470 /// - [`Error::UnexpectedEOF`] - if passed `file_content` data ends unexpectedly.
471 #[inline]
472 pub fn decrypt_in_place<'a>(
473 &'a mut self,
474 file_content: &'a mut [u8],
475 file_type: FileType,
476 ) -> Result<&'a [u8], Error> {
477 if !file_content.starts_with(&RPGM_HEADER) {
478 return Err(Error::InvalidHeader);
479 }
480
481 if !self.has_key {
482 self.set_key_from_file(file_content, file_type)?;
483 }
484
485 let sliced_past_header = &mut file_content[HEADER_LENGTH..];
486 self.xor_buffer(sliced_past_header);
487 Ok(sliced_past_header)
488 }
489
490 /// Encrypts file content.
491 ///
492 /// This function requires decrypter to have a key, which you can fetch from `System.json` file
493 /// or by calling [`Decrypter::set_key_from_file`] with the data from encrypted file.
494 ///
495 /// This function copies the contents of the file and returns encrypted [`Vec<u8>`] copy.
496 /// If you want to avoid copying, see [`Decrypter::encrypt_in_place`] function.
497 ///
498 /// # Arguments
499 ///
500 /// - `file_content` - The data of `.png`, `.ogg` or `.m4a` file.
501 ///
502 /// # Returns
503 ///
504 /// - [`Vec<u8>`] containing encrypted data if decrypter key is set.
505 /// - [`Error`] otherwise.
506 ///
507 /// # Errors
508 ///
509 /// - [`Error::KeyNotSet`] - if decrypter's key is not set.
510 #[inline]
511 pub fn encrypt(&self, file_content: &[u8]) -> Result<Vec<u8>, Error> {
512 if !self.has_key {
513 return Err(Error::KeyNotSet);
514 }
515
516 let mut data = file_content.to_vec();
517 self.xor_buffer(&mut data);
518
519 let mut output_data = Vec::with_capacity(HEADER_LENGTH + data.len());
520 output_data.extend(RPGM_HEADER);
521 output_data.extend(data);
522 Ok(output_data)
523 }
524
525 /// Encrypts file content in-place.
526 ///
527 /// This function requires decrypter to have a key, which you can fetch from `System.json` file
528 /// or by calling [`Decrypter::set_key_from_file`] with the data from encrypted file.
529 ///
530 /// This function encrypts the passed file data in-place.
531 /// If you don't want to modify passed data, see [`Decrypter::encrypt`] function.
532 ///
533 /// # Note
534 ///
535 /// 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.
536 /// The header is exported as [`RPGM_HEADER`].
537 ///
538 /// # Arguments
539 ///
540 /// - `file_content` - The data of `.png`, `.ogg` or `.m4a` file.
541 ///
542 /// # Returns
543 ///
544 /// - Nothing, if decrypter key is set.
545 /// - [`Error`] otherwise.
546 ///
547 /// # Errors
548 ///
549 /// - [`Error::KeyNotSet`] - if decrypter's key is not set.
550 #[inline]
551 pub fn encrypt_in_place(
552 &self,
553 file_content: &mut [u8],
554 ) -> Result<(), Error> {
555 if !self.has_key {
556 return Err(Error::KeyNotSet);
557 }
558
559 self.xor_buffer(file_content);
560 Ok(())
561 }
562}