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}