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}