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}