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#![doc = include_str!("../README.md")]
8
9use std::{
10    convert::TryFrom,
11    ffi::OsStr,
12    fmt::Display,
13    io::{Cursor, Read, Seek, SeekFrom},
14};
15use thiserror::Error;
16
17macro_rules! sizeof {
18    ($t:ty) => {{ size_of::<$t>() }};
19}
20
21const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
22
23pub const HEADER_LENGTH: usize = 16;
24
25pub const KEY_LENGTH: usize = 16;
26pub const KEY_STR_LENGTH: usize = 32;
27
28// Key used in RPG Maker encrypted files when "Encryption key" is left unfilled.
29pub const DEFAULT_KEY: &str = "d41d8cd98f00b204e9800998ecf8427e";
30
31// 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.
32
33// For PNG, header is always the same, so we can expect valid decryption.
34const PNG_HEADER: &[u8] = &[
35    0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
36    0x49, 0x48, 0x44, 0x52,
37];
38
39// 0 - 3 - OggS
40// 4 - version, always 0
41// 5 - header type, always 0x02, since first page always announces the beginning of the stream
42// 6 - 13 - granule position, always 0, since first page has no actual data
43//* 14 - 15 - part of 4-byte bitstream serial number, that actually differs between files
44static mut OGG_HEADER: [u8; HEADER_LENGTH] =
45    [79, 103, 103, 83, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
46
47//* 0 - 3 - type box size, actually differs between files
48// 4 - 7 - ftyp, always the same
49// 8 - 11 - M4A_, always the same, may be different 4 characters, but extremely unlikely
50// 12 - 15 - minor version, mostly junk, doesn't matter
51static mut M4A_HEADER: [u8; HEADER_LENGTH] =
52    [0, 0, 0, 28, 102, 116, 121, 112, 77, 52, 65, 32, 0, 0, 2, 0];
53
54// For finding type box size
55const M4A_POST_HEADER_BOXES: &[&[u8]] =
56    &[b"moov", b"mdat", b"free", b"skip", b"wide", b"pnot"];
57
58// Every encrypted file includes this header.
59pub const RPGM_HEADER: &[u8] = &[
60    0x52, 0x50, 0x47, 0x4d, 0x56, 0x00, 0x00, 0x00, 0x00, 0x03, 0x01, 0x00,
61    0x00, 0x00, 0x00, 0x00,
62];
63
64pub const MV_PNG_EXT: &str = "rpgmvp";
65pub const MZ_PNG_EXT: &str = "png_";
66pub const MV_OGG_EXT: &str = "rpgmvo";
67pub const MZ_OGG_EXT: &str = "ogg_";
68pub const MV_M4A_EXT: &str = "rpgmvm";
69pub const MZ_M4A_EXT: &str = "m4a_";
70
71pub const PNG_EXT: &str = "png";
72pub const OGG_EXT: &str = "ogg";
73pub const M4A_EXT: &str = "m4a";
74
75pub const ENCRYPTED_ASSET_EXTS: &[&str] = &[
76    MV_PNG_EXT, MV_OGG_EXT, MV_M4A_EXT, MZ_PNG_EXT, MZ_OGG_EXT, MZ_M4A_EXT,
77];
78pub const DECRYPTED_ASSETS_EXTS: &[&str] = &[PNG_EXT, OGG_EXT, M4A_EXT];
79
80#[derive(PartialEq, Clone, Copy)]
81#[repr(u8)]
82pub enum FileType {
83    PNG,
84    OGG,
85    M4A,
86}
87
88impl FileType {
89    #[must_use]
90    pub fn is_png(self) -> bool {
91        matches!(self, Self::PNG)
92    }
93
94    #[must_use]
95    pub fn is_ogg(self) -> bool {
96        matches!(self, Self::OGG)
97    }
98
99    #[must_use]
100    pub fn is_m4a(self) -> bool {
101        matches!(self, Self::M4A)
102    }
103}
104
105impl Display for FileType {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        match self {
108            Self::PNG => f.write_str("png"),
109            Self::OGG => f.write_str("ogg"),
110            Self::M4A => f.write_str("m4a"),
111        }
112    }
113}
114
115impl TryFrom<&str> for FileType {
116    type Error = &'static str;
117
118    fn try_from(value: &str) -> Result<Self, Self::Error> {
119        match value {
120            MV_PNG_EXT | MZ_PNG_EXT => Ok(FileType::PNG),
121            MV_OGG_EXT | MZ_OGG_EXT => Ok(FileType::OGG),
122            MV_M4A_EXT | MZ_M4A_EXT => Ok(FileType::M4A),
123            _ => Err("Extension not supported"),
124        }
125    }
126}
127
128// [`PathBuf::extension`] returns &OsStr, so implement this for convenience.
129impl TryFrom<&OsStr> for FileType {
130    type Error = &'static str;
131
132    fn try_from(value: &OsStr) -> Result<Self, Self::Error> {
133        if value == MV_PNG_EXT || value == MZ_PNG_EXT {
134            Ok(FileType::PNG)
135        } else if value == MV_OGG_EXT || value == MZ_OGG_EXT {
136            Ok(FileType::OGG)
137        } else if value == MV_M4A_EXT || value == MZ_M4A_EXT {
138            Ok(FileType::M4A)
139        } else {
140            Err("Extension not supported")
141        }
142    }
143}
144
145#[derive(Debug, Error)]
146#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
147pub enum Error {
148    #[error(
149        "Key must be set using any of `set_key` methods before calling `encrypt` function."
150    )]
151    KeyNotSet,
152    #[error("Key must have a fixed length of 32 characters.")]
153    InvalidKeyLength,
154    #[error(
155        "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."
156    )]
157    InvalidHeader,
158    #[error(
159        "Unexpected end of file encountered. Either passed data is not RPG Maker data or it's corrupted."
160    )]
161    UnexpectedEOF,
162}
163
164#[derive(Default)]
165pub struct Decrypter {
166    key_hex: [u8; KEY_STR_LENGTH],
167    key: [u8; KEY_LENGTH],
168    has_key: bool,
169}
170
171impl Decrypter {
172    /// Creates a new Decrypter instance.
173    ///
174    /// Decrypter requires a key, which you can set from [`Decrypter::set_key_from_str`] and [`Decrypter::set_key_from_file`] functions.
175    /// You can get the key string from `encryptionKey` field in `System.json` file or from any encrypted RPG Maker file.
176    ///
177    /// [`Decrypter::decrypt`] function will automatically determine the key from the input file, so you usually don't need to set it manually.
178    #[must_use]
179    pub fn new() -> Self {
180        Self::default()
181    }
182
183    #[inline]
184    /// Converts human-readable hex to the real key bytes.
185    fn set_key_from_hex(&mut self) {
186        for (j, i) in (0..self.key_hex.len()).step_by(2).enumerate() {
187            let u8_hex = [self.key_hex[i], self.key_hex[i + 1]];
188            let u8_hex_str = unsafe { std::str::from_utf8_unchecked(&u8_hex) };
189            self.key[j] = u8::from_str_radix(u8_hex_str, 16).unwrap();
190        }
191
192        self.has_key = true;
193    }
194
195    #[inline]
196    /// Either decrypts or encrypts the passed buffer, depending on the place this function was invoked from.
197    ///
198    /// Actual encryption is just: xor buffer's first 16 bytes with key.
199    fn xor_buffer(&self, buffer: &mut [u8]) {
200        for (i, item) in buffer.iter_mut().enumerate().take(HEADER_LENGTH) {
201            *item ^= self.key[i];
202        }
203    }
204
205    fn read_ogg_page_serialno(file_content: &mut Cursor<&[u8]>) -> u32 {
206        const HEADER_SIZE: usize = 27;
207        const SERIALNO_POS: usize = 14;
208
209        let mut header: [u8; HEADER_SIZE] = [0; HEADER_SIZE];
210
211        file_content.read_exact(&mut header).unwrap();
212
213        let segment_count: usize = header[26] as usize;
214        let mut segment_table: [u8; u8::MAX as usize] = [0; u8::MAX as usize];
215
216        file_content.read_exact(&mut segment_table).unwrap();
217
218        let over_count = i64::from(u8::MAX) - segment_count as i64;
219
220        file_content.seek(SeekFrom::Current(-over_count)).unwrap();
221
222        let mut body_length: i64 = 0;
223
224        for segment in segment_table.iter().take(segment_count) {
225            body_length += i64::from(*segment);
226        }
227
228        file_content.seek(SeekFrom::Current(body_length)).unwrap();
229
230        let header_serialno = unsafe {
231            *header[SERIALNO_POS..SERIALNO_POS + sizeof!(u32)]
232                .as_ptr()
233                .cast::<[u8; sizeof!(u32)]>()
234        };
235
236        u32::from(header_serialno[0])
237            | (u32::from(header_serialno[1]) << 8)
238            | (u32::from(header_serialno[2]) << 16)
239            | (u32::from(header_serialno[3]) << 24)
240    }
241
242    /// Returns the decrypter's key, or [`None`] if it's not set.
243    #[inline]
244    #[must_use]
245    pub fn key(&self) -> Option<&str> {
246        if !self.has_key {
247            return None;
248        }
249
250        Some(unsafe { std::str::from_utf8_unchecked(&self.key_hex) })
251    }
252
253    /// Sets the decrypter's key to provided `&str` hex string.
254    ///
255    /// # Returns
256    ///
257    /// If key's length is not 32 bytes, the function fails and returns [`Error`].
258    ///
259    /// # Errors
260    ///
261    /// - [`Error::InvalidKeyLength`] - if key's length is not 32 bytes.
262    #[inline]
263    pub fn set_key_from_str(&mut self, key: &str) -> Result<(), Error> {
264        if key.len() != KEY_STR_LENGTH {
265            return Err(Error::InvalidKeyLength);
266        }
267
268        self.key_hex =
269            unsafe { *key.as_bytes().as_ptr().cast::<[u8; KEY_STR_LENGTH]>() };
270        self.set_key_from_hex();
271
272        Ok(())
273    }
274
275    /// Sets the key of decrypter from encrypted `file_content` data.
276    ///
277    /// # Arguments
278    ///
279    /// - `file_content` - The data of RPG Maker file.
280    ///
281    /// # Returns
282    ///
283    /// - Reference to the key string, if succeeded.
284    /// - [`Error`] otherwise.
285    ///
286    /// # Errors
287    ///
288    /// - [`Error::InvalidHeader`] - if passed `file_content` data contains invalid header.
289    /// - [`Error::UnexpectedEOF`] - if passed `file_content` data ends unexpectedly.
290    #[inline]
291    pub fn set_key_from_file(
292        &mut self,
293        file_content: &[u8],
294        file_type: FileType,
295    ) -> Result<&str, Error> {
296        if !file_content.starts_with(RPGM_HEADER) {
297            return Err(Error::InvalidHeader);
298        }
299
300        let Some(post_header) =
301            file_content.get(HEADER_LENGTH..HEADER_LENGTH * 2)
302        else {
303            return Err(Error::UnexpectedEOF);
304        };
305
306        // Get proper M4A header box size
307        //* 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.
308        //* The same goes for 12-15 bytes (inclusive), they can be overwritten with whatever integer.
309        if file_type.is_m4a() {
310            const CHUNK_SIZE: usize = sizeof!(u32);
311
312            let Some(file_start) =
313                file_content.get(HEADER_LENGTH..HEADER_LENGTH + 64)
314            else {
315                return Err(Error::UnexpectedEOF);
316            };
317
318            let file_start_chunks = file_start.chunks_exact(CHUNK_SIZE);
319
320            for (i, chunk) in file_start_chunks.enumerate() {
321                if M4A_POST_HEADER_BOXES.contains(&chunk) {
322                    let prev_chunk_i = i - 1;
323                    let header_type_box_size =
324                        (prev_chunk_i * CHUNK_SIZE) as u32;
325
326                    unsafe {
327                        M4A_HEADER[..CHUNK_SIZE].copy_from_slice(
328                            &header_type_box_size.to_be_bytes(),
329                        );
330                    }
331                }
332            }
333        }
334
335        // 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.
336        // 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.
337        if file_type.is_ogg() {
338            let mut file_content_cursor =
339                Cursor::new(&file_content[HEADER_LENGTH..]);
340
341            Decrypter::read_ogg_page_serialno(&mut file_content_cursor);
342
343            let serialno =
344                Decrypter::read_ogg_page_serialno(&mut file_content_cursor);
345
346            unsafe {
347                OGG_HEADER[14..16]
348                    .clone_from_slice(&serialno.to_le_bytes()[0..2]);
349            }
350        }
351
352        let mut j = 0;
353        for i in 0..HEADER_LENGTH {
354            let signature_byte = match file_type {
355                FileType::PNG => PNG_HEADER[i],
356                FileType::OGG => unsafe { OGG_HEADER[i] },
357                FileType::M4A => unsafe { M4A_HEADER[i] },
358            };
359
360            let value = signature_byte ^ post_header[i];
361
362            let high = HEX_CHARS[(value >> 4) as usize];
363            let low = HEX_CHARS[(value & 0x0F) as usize];
364
365            self.key_hex[j] = high;
366            self.key_hex[j + 1] = low;
367            j += 2;
368        }
369
370        self.set_key_from_hex();
371        Ok(unsafe { std::str::from_utf8_unchecked(&self.key_hex) })
372    }
373
374    /// Decrypts RPG Maker file content.
375    /// Auto-determines the key from the input file.
376    ///
377    /// This function copies the contents of the file and returns decrypted [`Vec<u8>`] copy.
378    /// If you want to avoid copying, see [`Decrypter::decrypt_in_place`] function.
379    ///
380    /// # Arguments
381    ///
382    /// - `file_content` - The data of RPG Maker file.
383    /// - `file_type` - [`FileType`], representing whether passed file content is PNG, OGG or M4A.
384    ///
385    /// # Returns
386    ///
387    /// - [`Error`], if passed `file_content` data has invalid header.
388    /// - [`Vec<u8>`] containing decrypted data otherwise.
389    ///
390    /// # Errors
391    ///
392    /// - [`Error::InvalidHeader`] - if passed `file_content` data has invalid header.
393    /// - [`Error::UnexpectedEOF`] - if passed `file_content` data ends unexpectedly.
394    #[inline]
395    pub fn decrypt(
396        &mut self,
397        file_content: &[u8],
398        file_type: FileType,
399    ) -> Result<Vec<u8>, Error> {
400        if !file_content.starts_with(RPGM_HEADER) {
401            return Err(Error::InvalidHeader);
402        }
403
404        if !self.has_key {
405            self.set_key_from_file(file_content, file_type)?;
406        }
407
408        let mut result = file_content[HEADER_LENGTH..].to_vec();
409        self.xor_buffer(&mut result);
410        Ok(result)
411    }
412
413    /// Decrypts RPG Maker file content.
414    /// Auto-determines the key from the input file.
415    ///
416    /// This function decrypts the passed file data in-place.
417    /// If you don't want to modify passed data, see [`Decrypter::decrypt`] function.
418    ///
419    /// # Note
420    ///
421    /// Decrypted data is only valid starting at offset 16. This function returns the reference to the correct slice.
422    ///
423    /// # Arguments
424    ///
425    /// - `file_content` - The data of RPG Maker file.
426    /// - `file_type` - [`FileType`], representing whether passed file content is PNG, OGG or M4A.
427    ///
428    /// # Returns
429    ///
430    /// - [`Error`], if passed `file_content` data has invalid header.
431    /// - Reference to a slice of the passed `file_content` data starting at offset 16 otherwise.
432    ///
433    /// # Errors
434    ///
435    /// - [`Error::InvalidHeader`] - if passed `file_content` data has invalid header.
436    /// - [`Error::UnexpectedEOF`] - if passed `file_content` data ends unexpectedly.
437    #[inline]
438    pub fn decrypt_in_place<'a>(
439        &'a mut self,
440        file_content: &'a mut [u8],
441        file_type: FileType,
442    ) -> Result<&'a [u8], Error> {
443        if !file_content.starts_with(RPGM_HEADER) {
444            return Err(Error::InvalidHeader);
445        }
446
447        if !self.has_key {
448            self.set_key_from_file(file_content, file_type)?;
449        }
450
451        let sliced_past_header = &mut file_content[HEADER_LENGTH..];
452        self.xor_buffer(sliced_past_header);
453        Ok(sliced_past_header)
454    }
455
456    /// Encrypts file content.
457    ///
458    /// This function requires decrypter to have a key, which you can fetch from `System.json` file
459    /// or by calling [`Decrypter::set_key_from_file`] with the data from encrypted file.
460    ///
461    /// This function copies the contents of the file and returns encrypted [`Vec<u8>`] copy.
462    /// If you want to avoid copying, see [`Decrypter::encrypt_in_place`] function.
463    ///
464    /// # Arguments
465    ///
466    /// - `file_content` - The data of `.png`, `.ogg` or `.m4a` file.
467    ///
468    /// # Returns
469    ///
470    /// - [`Vec<u8>`] containing encrypted data if decrypter key is set.
471    /// - [`Error`] otherwise.
472    ///
473    /// # Errors
474    ///
475    /// - [`Error::KeyNotSet`] - if decrypter's key is not set.
476    #[inline]
477    pub fn encrypt(&self, file_content: &[u8]) -> Result<Vec<u8>, Error> {
478        if !self.has_key {
479            return Err(Error::KeyNotSet);
480        }
481
482        let mut data = file_content.to_vec();
483        self.xor_buffer(&mut data);
484
485        let mut output_data = Vec::with_capacity(HEADER_LENGTH + data.len());
486        output_data.extend(RPGM_HEADER);
487        output_data.extend(data);
488        Ok(output_data)
489    }
490
491    /// Encrypts file content in-place.
492    ///
493    /// This function requires decrypter to have a key, which you can fetch from `System.json` file
494    /// or by calling [`Decrypter::set_key_from_file`] with the data from encrypted file.
495    ///
496    /// This function encrypts the passed file data in-place.
497    /// If you don't want to modify passed data, see [`Decrypter::encrypt`] function.
498    ///
499    /// # Note
500    ///
501    /// 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.
502    /// The header is exported as [`RPGM_HEADER`].
503    ///
504    /// # Arguments
505    ///
506    /// - `file_content` - The data of `.png`, `.ogg` or `.m4a` file.
507    ///
508    /// # Returns
509    ///
510    /// - Nothing, if decrypter key is set.
511    /// - [`Error`] otherwise.
512    ///
513    /// # Errors
514    ///
515    /// - [`Error::KeyNotSet`] - if decrypter's key is not set.
516    #[inline]
517    pub fn encrypt_in_place(
518        &self,
519        file_content: &mut [u8],
520    ) -> Result<(), Error> {
521        if !self.has_key {
522            return Err(Error::KeyNotSet);
523        }
524
525        self.xor_buffer(file_content);
526        Ok(())
527    }
528}
529
530/// Decrypts RPG Maker file content using a temporary [`Decrypter`] instance.
531///
532/// This is a convenience wrapper around [`Decrypter::decrypt`].
533/// A new [`Decrypter`] is created internally, and the decryption key is
534/// auto-determined from the provided file data.
535///
536/// This function copies the contents of the file and returns a decrypted [`Vec<u8>`].
537/// If you want to avoid copying, use [`decrypt_in_place`] instead.
538///
539/// # Arguments
540///
541/// - `file_content` - The data of RPG Maker file.
542/// - `file_type` - [`FileType`], representing whether passed file content is PNG, OGG or M4A.
543///
544/// # Returns
545///
546/// - [`Error`] if the passed data has an invalid header or ends unexpectedly.
547/// - Decrypted [`Vec<u8>`] otherwise.
548///
549/// # Errors
550///
551/// - [`Error::InvalidHeader`] – if the provided `file_content` does not start with the RPG Maker header.
552/// - [`Error::UnexpectedEOF`] – if the data ends unexpectedly.
553pub fn decrypt(
554    file_content: &[u8],
555    file_type: FileType,
556) -> Result<Vec<u8>, Error> {
557    Decrypter::new().decrypt(file_content, file_type)
558}
559
560/// Decrypts RPG Maker file content in-place using a temporary [`Decrypter`] instance.
561///
562/// This is a convenience wrapper around [`Decrypter::decrypt_in_place`].
563/// A new [`Decrypter`] is created internally, and the decryption key is
564/// auto-determined from the provided file data.
565///
566/// This function modifies the provided buffer directly.
567/// After successful decryption, the decrypted data is valid starting at offset 16.
568///
569/// If you do not want to modify data in-place, use [`decrypt`] instead.
570///
571/// # Arguments
572///
573/// - `file_content` - The data of RPG Maker file.
574/// - `file_type` - [`FileType`], representing whether passed file content is PNG, OGG or M4A.
575///
576/// # Returns
577///
578/// - [`Error`] if the passed data has an invalid header or ends unexpectedly.
579/// - Nothing otherwise.
580///
581/// # Errors
582///
583/// - [`Error::InvalidHeader`] – if the provided `file_content` does not start with the RPG Maker header.
584/// - [`Error::UnexpectedEOF`] – if the data ends unexpectedly.
585pub fn decrypt_in_place(
586    file_content: &mut [u8],
587    file_type: FileType,
588) -> Result<(), Error> {
589    Decrypter::new().decrypt_in_place(file_content, file_type)?;
590    Ok(())
591}
592
593/// Encrypts file content using a key string and a temporary [`Decrypter`] instance.
594///
595/// This is a convenience wrapper around [`Decrypter::encrypt`].
596/// A new [`Decrypter`] is created internally, and the key is set from the provided string.
597///
598/// This function copies the file contents and returns an encrypted [`Vec<u8>`].
599/// The output includes the RPG Maker encryption header (`RPGM_HEADER`).
600///
601/// If you want to avoid copying, use [`encrypt_in_place`] instead.
602///
603/// # Arguments
604///
605/// - `file_content` - The data of `.png`, `.ogg` or `.m4a` file.
606/// - `key` - Encryption key string.
607///
608/// # Returns
609///
610/// - Encrypted [`Vec<u8>`] if the key is valid.
611/// - [`Error`] otherwise.
612///
613/// # Errors
614///
615/// - [`Error::InvalidKeyLength`] - if key's length is not 32 bytes.
616/// - [`Error::KeyNotSet`] – if key initialization fails.
617pub fn encrypt(file_content: &[u8], key: &str) -> Result<Vec<u8>, Error> {
618    let mut decrypter = Decrypter::new();
619    decrypter.set_key_from_str(key)?;
620    decrypter.encrypt(file_content)
621}
622
623/// Encrypts file content in-place using a key string and a temporary [`Decrypter`] instance.
624///
625/// This is a convenience wrapper around [`Decrypter::encrypt_in_place`].
626/// A new [`Decrypter`] is created internally, and the key is set from the provided string.
627///
628/// This function modifies the file data directly and produces *only* the encrypted payload.
629/// The RPG Maker encryption header is **not** added automatically; it must be prepended manually
630/// if producing a complete `.rpgmvp`, `.rpgmvo`, or `.rpgmvw` file.
631///
632/// If you do not want to modify data in-place, use [`encrypt`] instead.
633///
634/// # Arguments
635///
636/// - `file_content` - The data of `.png`, `.ogg` or `.m4a` file.
637/// - `key` - Encryption key string.
638///
639/// # Returns
640///
641/// - [`Error`] if key's length is not 32 bytes.
642/// - Nothing otherwise.
643///
644/// # Errors
645///
646/// - [`Error::InvalidKeyLength`] - if key's length is not 32 bytes.
647pub fn encrypt_in_place(
648    file_content: &mut [u8],
649    key: &str,
650) -> Result<(), Error> {
651    let mut decrypter = Decrypter::new();
652    decrypter.set_key_from_str(key)?;
653    decrypter.encrypt_in_place(file_content)?;
654    Ok(())
655}