Skip to main content

rpfm_lib/encryption/
mod.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! This module contains the code to encrypt and decrypt data in Total War PackFiles.
12//!
13//! The [`Decryptable`] trait provides functions to decrypt various parts of encrypted PackFiles,
14//! including file data, file sizes, and file paths. An implementation for anything that implements
15//! [`ReadBytes`] + [`Read`] + [`Seek`] is provided.
16//!
17//! The [`Encryptable`] trait provides the inverse operations. An implementation for anything that
18//! implements [`WriteBytes`] is provided.
19//!
20//! # Encryption Scheme
21//!
22//! Total War games use a custom encryption scheme with different keys for different parts of the PackFile:
23//! - **Index String Key**: 64-byte key for decrypting file paths
24//! - **Index U32 Key**: [`u32`] key for decrypting file sizes
25//! - **Data Key**: [`u64`] key for decrypting file data (combined with a [`u32`] position in PFH4 and older Packs)
26//!
27//! # Historical Context
28//!
29//! The encryption scheme has evolved over time:
30//! - **Shogun 2 to Arena**: Used older keys (now commented out in the code)
31//! - **Modern Games**: Use the current key set introduced after Arena
32//!
33//! [`Read`]: std::io::Read
34//! [`Seek`]: std::io::Seek
35
36use std::io::{Read, Seek};
37
38use crate::error::Result;
39use crate::binary::{ReadBytes, WriteBytes};
40use crate::games::pfh_version::PFHVersion;
41
42#[cfg(test)] mod encryption_test;
43
44// Old 64-byte key used in Arena and all the way back to Shogun 2 for decrypting file paths.
45// This key is no longer used but is kept for reference and backwards compatibility with older PackFiles.
46// static INDEX_STRING_KEY: &str = "L2{B3dPL7L*v&+Q3ZsusUhy[BGQn(Uq$f>JQdnvdlf{-K:>OssVDr#TlYU|13B}r";
47
48// Old u32 key used in Arena's encrypted PackFiles for decrypting file sizes.
49// This key is no longer used but is kept for reference and backwards compatibility with older PackFiles.
50// static INDEX_U32_KEY: u32 = 0x1509_1984;
51
52/// Current 64-byte key used for encrypting and decrypting PackedFile paths in the encrypted index.
53///
54/// This key rotates through its 64 bytes during the decryption process. Each character of the
55/// encrypted path is XORed with the corresponding byte from this key (wrapping around after 64 bytes).
56static INDEX_STRING_KEY: [u8; 64] = *b"#:AhppdV-!PEfz&}[]Nv?6w4guU%dF5.fq:n*-qGuhBJJBm&?2tPy!geW/+k#pG?";
57
58/// Current [`u32`] key used for encrypting and decrypting PackedFile sizes in the encrypted index.
59///
60/// This key is combined with a position-based secondary key during the XOR decryption process.
61static INDEX_U32_KEY: u32 = 0xE10B_73F4;
62
63/// Current [`u64`] key used for encrypting and decrypting PackedFile data.
64///
65/// This key is used in 8-byte chunks to decrypt the actual file data. The decryption
66/// formula is: `decrypted = encrypted XOR (DATA_KEY * !position)`, with `!position` computed
67/// as a [`u64`] in PFH5+ Packs and as a [`u32`] in PFH4 and older Packs.
68static DATA_KEY: u64 = 0x8FEB_2A67_40A6_920E;
69
70/// Trait for decrypting encrypted PackFile data.
71///
72/// This trait provides methods to decrypt different parts of encrypted PackFiles using
73/// Total War's custom encryption scheme. The decryption uses three different keys for
74/// different types of data (file data, file sizes, and file paths).
75///
76/// # Implementation
77///
78/// This trait is automatically implemented for any type that implements
79/// [`ReadBytes`] + [`Read`] + [`Seek`].
80///
81/// [`Read`]: std::io::Read
82/// [`Seek`]: std::io::Seek
83pub trait Decryptable: ReadBytes + Read + Seek {
84
85    /// Decrypts the data of an encrypted PackedFile.
86    ///
87    /// Data is decrypted in 8-byte chunks using the DATA_KEY. Trailing bytes that don't fill a full chunk are not encrypted.
88    ///
89    /// # Arguments
90    ///
91    /// * `pfh_version` - The version of the Pack the data comes from. Determines the width of the position term in the key.
92    ///
93    /// # Returns
94    ///
95    /// A [`Vec<u8>`] containing the decrypted data, or an error if reading fails.
96    ///
97    /// # Examples
98    ///
99    /// ```ignore
100    /// use std::io::Cursor;
101    /// use rpfm_lib::encryption::Decryptable;
102    /// use rpfm_lib::games::pfh_version::PFHVersion;
103    ///
104    /// let encrypted_data = vec![/* encrypted bytes */];
105    /// let mut cursor = Cursor::new(encrypted_data);
106    /// let decrypted = cursor.decrypt(PFHVersion::PFH5)?;
107    /// ```
108    fn decrypt(&mut self, pfh_version: PFHVersion) -> Result<Vec<u8>> {
109        let ciphertext_len = self.len()? as usize;
110        let ciphertext = self.read_slice(ciphertext_len, false)?;
111        let mut plaintext = Vec::with_capacity(ciphertext.len());
112        apply_data_key(&ciphertext, pfh_version, &mut plaintext);
113        Ok(plaintext)
114    }
115
116    /// Decrypts the size of a PackedFile from the encrypted index.
117    ///
118    /// This function reads and decrypts a [`u32`] value representing the size of a PackedFile.
119    /// The decryption uses both the INDEX_U32_KEY and a second key derived from the position
120    /// in the index.
121    ///
122    /// # Arguments
123    ///
124    /// * `second_key` - The secondary key, typically the number of PackedFiles after this one in the index.
125    ///
126    /// # Returns
127    ///
128    /// The decrypted file size as a [`u32`], or an error if reading fails.
129    ///
130    /// # Examples
131    ///
132    /// ```ignore
133    /// use std::io::Cursor;
134    /// use rpfm_lib::encryption::Decryptable;
135    ///
136    /// let encrypted_index = vec![/* encrypted index bytes */];
137    /// let mut cursor = Cursor::new(encrypted_index);
138    /// let packed_files_remaining = 5;
139    /// let size = cursor.decrypt_u32(packed_files_remaining)?;
140    /// ```
141    fn decrypt_u32(&mut self, second_key: u32) -> Result<u32> {
142        let bytes = self.read_u32()?;
143        Ok(bytes ^ INDEX_U32_KEY ^ !second_key)
144    }
145
146    /// Decrypts the path of a PackedFile from the encrypted index.
147    ///
148    /// This function reads and decrypts a null-terminated string representing the path of a PackedFile.
149    /// The decryption uses the INDEX_STRING_KEY (a 64-byte rotating key) and a second key derived
150    /// from the file's properties.
151    ///
152    /// # Arguments
153    ///
154    /// * `second_key` - The secondary key, typically derived from the file's decrypted size.
155    ///
156    /// # Returns
157    ///
158    /// The decrypted file path as a [`String`], or an error if reading fails.
159    ///
160    /// # Implementation Note
161    ///
162    /// This function reads the encrypted string byte-by-byte until a null terminator (0x00) is found.
163    /// Each byte is XORed with the corresponding byte from INDEX_STRING_KEY (rotating through the 64-byte key)
164    /// and the inverted second_key.
165    ///
166    /// # Examples
167    ///
168    /// ```ignore
169    /// use std::io::Cursor;
170    /// use rpfm_lib::encryption::Decryptable;
171    ///
172    /// let encrypted_index = vec![/* encrypted index bytes */];
173    /// let mut cursor = Cursor::new(encrypted_index);
174    /// let file_size = 1024u8;
175    /// let path = cursor.decrypt_string(file_size)?;
176    /// ```
177    fn decrypt_string(&mut self, second_key: u8) -> Result<String> {
178        let mut path: String = String::new();
179        let mut index = 0;
180        loop {
181            let character = self.read_u8()? ^ INDEX_STRING_KEY[index % INDEX_STRING_KEY.len()] ^ !second_key;
182            index += 1;
183            if character == 0 { break; }
184            path.push(character as char);
185        }
186        Ok(path)
187    }
188}
189
190impl<R: ReadBytes + Read + Seek> Decryptable for R {}
191
192/// Trait for encrypting PackFile data.
193///
194/// This trait provides the inverse operations of [`Decryptable`], writing the encrypted
195/// data into the implementor.
196///
197/// # Implementation
198///
199/// This trait is automatically implemented for any type that implements [`WriteBytes`].
200pub trait Encryptable: WriteBytes {
201
202    /// Encrypts the data of a PackedFile and writes it.
203    ///
204    /// Data is encrypted in 8-byte chunks using the DATA_KEY. Trailing bytes that don't fill a full chunk are written unencrypted.
205    ///
206    /// # Arguments
207    ///
208    /// * `data` - The plain data to encrypt.
209    /// * `pfh_version` - The version of the Pack the data is for. Determines the width of the position term in the key.
210    ///
211    /// # Errors
212    ///
213    /// Returns an error if writing fails.
214    fn encrypt(&mut self, data: &[u8], pfh_version: PFHVersion) -> Result<()> {
215        let mut ciphertext = Vec::with_capacity(data.len());
216        apply_data_key(data, pfh_version, &mut ciphertext);
217        self.write_all(&ciphertext)?;
218        Ok(())
219    }
220
221    /// Encrypts a [`u32`] of a PackedFile's index entry (size or timestamp) and writes it.
222    ///
223    /// # Arguments
224    ///
225    /// * `value` - The plain value to encrypt.
226    /// * `second_key` - The secondary key, the number of PackedFiles after this one in the index.
227    ///
228    /// # Errors
229    ///
230    /// Returns an error if writing fails.
231    fn encrypt_u32(&mut self, value: u32, second_key: u32) -> Result<()> {
232        self.write_u32(value ^ INDEX_U32_KEY ^ !second_key)
233    }
234
235    /// Encrypts the path of a PackedFile, including its null terminator, and writes it.
236    ///
237    /// # Arguments
238    ///
239    /// * `path` - The plain path to encrypt.
240    /// * `second_key` - The secondary key, the lowest byte of the PackedFile's size in the Pack.
241    ///
242    /// # Errors
243    ///
244    /// Returns an error if writing fails.
245    fn encrypt_string(&mut self, path: &str, second_key: u8) -> Result<()> {
246        let ciphertext = path.bytes()
247            .chain(std::iter::once(0))
248            .enumerate()
249            .map(|(index, character)| character ^ INDEX_STRING_KEY[index % INDEX_STRING_KEY.len()] ^ !second_key)
250            .collect::<Vec<u8>>();
251        self.write_all(&ciphertext)?;
252        Ok(())
253    }
254}
255
256impl<W: WriteBytes> Encryptable for W {}
257
258/// XORs data with the DATA_KEY stream, appending the result to `output`.
259///
260/// The operation is its own inverse, so it's used for both encryption and decryption.
261///
262/// # Arguments
263///
264/// * `input` - The data to transform.
265/// * `pfh_version` - The version of the Pack the data belongs to. Determines the width of the position term in the key.
266/// * `output` - Buffer the transformed data is appended to.
267fn apply_data_key(input: &[u8], pfh_version: PFHVersion, output: &mut Vec<u8>) {
268
269    // PFH4 and older games negate the position as a u32 before widening it to u64.
270    let wide_position = match pfh_version {
271        PFHVersion::PFH6 | PFHVersion::PFH5 => true,
272        PFHVersion::PFH4 | PFHVersion::PFH3 | PFHVersion::PFH2 | PFHVersion::PFH0 => false,
273    };
274
275    let (chunks, remainder) = input.as_chunks::<8>();
276    for (index, chunk) in chunks.iter().enumerate() {
277        let position = index as u64 * 8;
278        let negated_position = if wide_position { !position } else { u64::from(!(position as u32)) };
279        let key = DATA_KEY.wrapping_mul(negated_position);
280        output.extend_from_slice(&(u64::from_le_bytes(*chunk) ^ key).to_le_bytes());
281    }
282
283    output.extend_from_slice(remainder);
284}