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 decrypt encrypted 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//! # Encryption Scheme
18//!
19//! Total War games use a custom encryption scheme with different keys for different parts of the PackFile:
20//! - **Index String Key**: 64-byte key for decrypting file paths
21//! - **Index U32 Key**: [`u32`] key for decrypting file sizes
22//! - **Data Key**: [`u64`] key for decrypting file data
23//!
24//! # Historical Context
25//!
26//! The encryption scheme has evolved over time:
27//! - **Shogun 2 to Arena**: Used older keys (now commented out in the code)
28//! - **Modern Games**: Use the current key set introduced after Arena
29//!
30//! [`Read`]: std::io::Read
31//! [`Seek`]: std::io::Seek
32
33use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
34
35use std::io::{Read, Seek};
36
37use crate::error::Result;
38use crate::binary::ReadBytes;
39
40/// Old 64-byte key used in Arena and all the way back to Shogun 2 for decrypting file paths.
41///
42/// This key is no longer used but is kept for reference and backwards compatibility with older PackFiles.
43// static INDEX_STRING_KEY: &str = "L2{B3dPL7L*v&+Q3ZsusUhy[BGQn(Uq$f>JQdnvdlf{-K:>OssVDr#TlYU|13B}r";
44
45/// Old [`u32`] key used in Arena's encrypted PackFiles for decrypting file sizes.
46///
47/// This key is no longer used but is kept for reference and backwards compatibility with older PackFiles.
48// static INDEX_U32_KEY: u32 = 0x1509_1984;
49
50/// Current 64-byte key used for decrypting PackedFile paths in the encrypted index.
51///
52/// This key rotates through its 64 bytes during the decryption process. Each character of the
53/// encrypted path is XORed with the corresponding byte from this key (wrapping around after 64 bytes).
54static INDEX_STRING_KEY: [u8; 64] = *b"#:AhppdV-!PEfz&}[]Nv?6w4guU%dF5.fq:n*-qGuhBJJBm&?2tPy!geW/+k#pG?";
55
56/// Current [`u32`] key used for decrypting PackedFile sizes in the encrypted index.
57///
58/// This key is combined with a position-based secondary key during the XOR decryption process.
59static INDEX_U32_KEY: u32 = 0xE10B_73F4;
60
61/// Current [`u64`] key used for decrypting PackedFile data.
62///
63/// This key is used in 8-byte chunks to decrypt the actual file data. The decryption
64/// formula is: `decrypted = encrypted XOR (DATA_KEY * !position)`.
65static DATA_KEY: u64 = 0x8FEB_2A67_40A6_920E;
66
67/// Trait for decrypting encrypted PackFile data.
68///
69/// This trait provides methods to decrypt different parts of encrypted PackFiles using
70/// Total War's custom encryption scheme. The decryption uses three different keys for
71/// different types of data (file data, file sizes, and file paths).
72///
73/// # Implementation
74///
75/// This trait is automatically implemented for any type that implements
76/// [`ReadBytes`] + [`Read`] + [`Seek`].
77///
78/// [`Read`]: std::io::Read
79/// [`Seek`]: std::io::Seek
80pub trait Decryptable: ReadBytes + Read + Seek {
81
82    /// Decrypts the data of an encrypted PackedFile.
83    ///
84    /// This function decrypts data in 8-byte chunks using the DATA_KEY. The file is first
85    /// padded to a multiple of 8 bytes if needed, then decrypted chunk by chunk. Note that
86    /// the last chunk is NOT encrypted and is copied as-is.
87    ///
88    /// # Returns
89    ///
90    /// A [`Vec<u8>`] containing the decrypted data, or an error if decryption fails.
91    ///
92    /// # Examples
93    ///
94    /// ```ignore
95    /// use std::io::Cursor;
96    /// use rpfm_lib::encryption::Decryptable;
97    ///
98    /// let encrypted_data = vec![/* encrypted bytes */];
99    /// let mut cursor = Cursor::new(encrypted_data);
100    /// let decrypted = cursor.decrypt()?;
101    /// ```
102    fn decrypt(&mut self) -> Result<Vec<u8>> {
103
104        // First, make sure the file ends in a multiple of 8. If not, extend it with zeros.
105        // We need it because the decoding is done in packs of 8 bytes.
106        let ciphertext_len = self.len()? as usize;
107        let mut ciphertext = self.read_slice(ciphertext_len, false)?;
108        let size = ciphertext.len();
109        let padding = 8 - (size % 8);
110        if padding < 8 {
111            ciphertext.resize(size + padding, 0);
112        }
113
114        // Then decrypt the file in packs of 8. It's faster than in packs of 4.
115        let mut plaintext = Vec::with_capacity(ciphertext.len());
116        let mut edi: u64 = 0;
117        let chunks = ciphertext.len() / 8;
118        for i in 0..chunks {
119
120            // The last chunk is NOT ENCRYPTED.
121            let esi = edi as usize;
122            if i == chunks - 1 {
123                plaintext.extend_from_slice(&ciphertext[esi..esi + 8]);
124            } else {
125                let mut prod = DATA_KEY.wrapping_mul(!edi);
126                prod ^= (&ciphertext[esi..esi + 8]).read_u64::<LittleEndian>().unwrap();
127                plaintext.write_u64::<LittleEndian>(prod).unwrap();
128            }
129            edi += 8
130        }
131
132        // Remove the extra bytes we added in the first step.
133        plaintext.truncate(size);
134        Ok(plaintext)
135    }
136
137    /// Decrypts the size of a PackedFile from the encrypted index.
138    ///
139    /// This function reads and decrypts a [`u32`] value representing the size of a PackedFile.
140    /// The decryption uses both the INDEX_U32_KEY and a second key derived from the position
141    /// in the index.
142    ///
143    /// # Arguments
144    ///
145    /// * `second_key` - The secondary key, typically the number of PackedFiles after this one in the index.
146    ///
147    /// # Returns
148    ///
149    /// The decrypted file size as a [`u32`], or an error if reading fails.
150    ///
151    /// # Examples
152    ///
153    /// ```ignore
154    /// use std::io::Cursor;
155    /// use rpfm_lib::encryption::Decryptable;
156    ///
157    /// let encrypted_index = vec![/* encrypted index bytes */];
158    /// let mut cursor = Cursor::new(encrypted_index);
159    /// let packed_files_remaining = 5;
160    /// let size = cursor.decrypt_u32(packed_files_remaining)?;
161    /// ```
162    fn decrypt_u32(&mut self, second_key: u32) -> Result<u32> {
163        let bytes = self.read_u32()?;
164        Ok(bytes ^ INDEX_U32_KEY ^ !second_key)
165    }
166
167    /// Decrypts the path of a PackedFile from the encrypted index.
168    ///
169    /// This function reads and decrypts a null-terminated string representing the path of a PackedFile.
170    /// The decryption uses the INDEX_STRING_KEY (a 64-byte rotating key) and a second key derived
171    /// from the file's properties.
172    ///
173    /// # Arguments
174    ///
175    /// * `second_key` - The secondary key, typically derived from the file's decrypted size.
176    ///
177    /// # Returns
178    ///
179    /// The decrypted file path as a [`String`], or an error if reading fails.
180    ///
181    /// # Implementation Note
182    ///
183    /// This function reads the encrypted string byte-by-byte until a null terminator (0x00) is found.
184    /// Each byte is XORed with the corresponding byte from INDEX_STRING_KEY (rotating through the 64-byte key)
185    /// and the inverted second_key.
186    ///
187    /// # Examples
188    ///
189    /// ```ignore
190    /// use std::io::Cursor;
191    /// use rpfm_lib::encryption::Decryptable;
192    ///
193    /// let encrypted_index = vec![/* encrypted index bytes */];
194    /// let mut cursor = Cursor::new(encrypted_index);
195    /// let file_size = 1024u8;
196    /// let path = cursor.decrypt_string(file_size)?;
197    /// ```
198    fn decrypt_string(&mut self, second_key: u8) -> Result<String> {
199        let mut path: String = String::new();
200        let mut index = 0;
201        loop {
202            let character = self.read_u8()? ^ INDEX_STRING_KEY[index % INDEX_STRING_KEY.len()] ^ !second_key;
203            index += 1;
204            if character == 0 { break; }
205            path.push(character as char);
206        }
207        Ok(path)
208    }
209}
210
211impl<R: ReadBytes + Read + Seek> Decryptable for R {}