Skip to main content

rpkg_rs/encryption/
xtea.rs

1use crate::encryption::xtea::XteaError::InvalidInput;
2use byteorder::{LittleEndian, WriteBytesExt};
3use extended_tea::XTEA;
4use std::io::Cursor;
5use thiserror::Error;
6
7/// Errors that can occur during XTEA encryption or decryption.
8#[derive(Debug, Error)]
9pub enum XteaError {
10    #[error("Text encoding error: {0}")]
11    TextEncodingError(std::string::FromUtf8Error),
12
13    #[error("An error occurred while trying to decrypt the file: {:?}", _0)]
14    CipherError(std::io::Error),
15
16    #[error("Unexpected input: {:?}", _0)]
17    InvalidInput(String),
18
19    #[error("Xtea encoding error: {0}")]
20    XteaEncodingError(std::io::Error),
21}
22
23/// Implementation of XTEA encryption and decryption methods.
24pub struct Xtea;
25
26impl Xtea {
27    /// Default XTEA key, used in thumbs.dat and packagedefinition.txt
28    #[deprecated(
29        since = "1.4.0",
30        note = "Why are you using this directly? Please keep a local copy, as this const will be private in the next release"
31    )]
32    pub const DEFAULT_KEY: [u32; 4] = [0x30f95282, 0x1f48c419, 0x295f8548, 0x2a78366d];
33
34    const WOA_KEY: [u32; 4] = [0x30f95282, 0x1f48c419, 0x295f8548, 0x2a78366d];
35    const BOND_KEY: [u32; 4] = [0x71482CF0, 0x5FDC4B9F, 0x86CE569D, 0x509FC1E];
36
37    /// LOCR/TEXTLIST XTEA key, used for localization
38    #[deprecated(
39        since = "1.4.0",
40        note = "LOCR_KEY is now ambiguous, please sure either WOA_L10N_KEY or BOND_L10N_KEY"
41    )]
42    pub const LOCR_KEY: [u32; 4] = [0x53527737, 0x7506499E, 0xBD39AEE3, 0xA59E7268];
43
44    pub const WOA_L10N_KEY: [u32; 4] = [0x53527737, 0x7506499E, 0xBD39AEE3, 0xA59E7268];
45
46    pub const BOND_L10N_KEY: [u32; 4] = [0x68AC3361, 0x562B4AA0, 0xB9F2771F, 0x28EB3CE7];
47
48
49    /// Default header for encrypted files.
50    const WOA_ENCRYPTED_HEADER: [u8; 0x10] = [
51        0x22, 0x3d, 0x6f, 0x9a, 0xb3, 0xf8, 0xfe, 0xb6, 0x61, 0xd9, 0xcc, 0x1c, 0x62, 0xde, 0x83,
52        0x41,
53    ];
54
55    const BOND_ENCRYPTED_HEADER: [u8; 0x10] = [
56        0xB7, 0xE2, 0xEA, 0x00, 0x54, 0x5B, 0x6B, 0x87, 0x11, 0xBD, 0x6F, 0xE8, 0x4D, 0x6A, 0xD4,
57        0xBF,
58    ];
59
60    /// Checks if a given buffer represents an encrypted text file.
61    /// This function will check for the presence of a default header in the text file.
62    pub fn is_encrypted_text_file(input_buffer: &[u8]) -> bool {
63        input_buffer.starts_with(&Self::WOA_ENCRYPTED_HEADER) ||
64            input_buffer.starts_with(&Self::BOND_ENCRYPTED_HEADER)
65    }
66
67    /// Decrypts a text file given its buffer, uses the default xtea key.
68    pub fn decrypt_text_file(input_buffer: &[u8]) -> Result<String, XteaError> {
69        let payload_start = Self::WOA_ENCRYPTED_HEADER.len() + 4;
70
71        if input_buffer.len() < payload_start {
72            return Err(InvalidInput("Input too short".to_string()));
73        }
74
75        if !Self::is_encrypted_text_file(input_buffer) {
76            return Err(InvalidInput("Header mismatch".to_string()));
77        }
78        let checksum = &input_buffer[payload_start - 4..payload_start];
79        let input = &input_buffer[payload_start..];
80
81        if !input.len().is_multiple_of(8) {
82            return Err(InvalidInput(
83                "Input must be of a length divisible by 8".to_string(),
84            ));
85        }
86
87        let mut key = Self::WOA_KEY;
88        if input_buffer.starts_with(&Self::BOND_ENCRYPTED_HEADER){
89            key = Self::BOND_KEY
90        }
91
92        let xtea = XTEA::new(&key);
93        let mut out_buffer = vec![0u8; input.len()];
94
95        let mut input_reader = Cursor::new(input);
96        let mut ouput_writer = Cursor::new(&mut out_buffer);
97
98        xtea.decipher_stream::<LittleEndian, _, _>(&mut input_reader, &mut ouput_writer)
99            .map_err(XteaError::CipherError)?;
100
101        let output = String::from_utf8(ouput_writer.get_mut().to_owned())
102            .map_err(XteaError::TextEncodingError)?;
103
104        let result_checksum =
105            crc32fast::hash(output.trim_end_matches('\0').as_bytes()).to_le_bytes();
106        match result_checksum == checksum {
107            true => Ok(output),
108            false => Err(InvalidInput("CRC checksum mismatched!".to_string())),
109        }
110    }
111
112    /// Decrypts a string given its buffer and key.
113    pub fn decrypt_string(input_buffer: &[u8], key: &[u32; 4]) -> Result<String, XteaError> {
114        let input = &input_buffer;
115
116        if !input.len().is_multiple_of(8) {
117            return Err(InvalidInput(
118                "Input must be of a length divisible by 8".to_string(),
119            ));
120        }
121
122        let xtea = XTEA::new(key);
123        let mut out_buffer = vec![0u8; input.len()];
124
125        let mut input_reader = Cursor::new(input);
126        let mut ouput_writer = Cursor::new(&mut out_buffer);
127
128        xtea.decipher_stream::<LittleEndian, _, _>(&mut input_reader, &mut ouput_writer)
129            .map_err(XteaError::CipherError)?;
130
131        String::from_utf8(ouput_writer.get_mut().to_owned()).map_err(XteaError::TextEncodingError)
132    }
133
134    #[deprecated(
135        since = "1.4.0",
136        note = "encrypt_text_file is only able to encrypt for Woa, please move to either encrypt_woa_text_file or encrypt_bond_text_file"
137    )]
138    pub fn encrypt_text_file(input_string: String) -> Result<Vec<u8>, XteaError> {
139        Self::encrypt_text_file_internal(input_string, Self::WOA_KEY, Self::WOA_ENCRYPTED_HEADER)
140    }
141
142    pub fn encrypt_woa_text_file(input_string: String) -> Result<Vec<u8>, XteaError> {
143        Self::encrypt_text_file_internal(input_string, Self::WOA_KEY, Self::WOA_ENCRYPTED_HEADER)
144    }
145
146    pub fn encrypt_bond_text_file(input_string: String) -> Result<Vec<u8>, XteaError> {
147        Self::encrypt_text_file_internal(input_string, Self::BOND_KEY, Self::BOND_ENCRYPTED_HEADER)
148    }
149
150    fn encrypt_text_file_internal(input_string: String, key: [u32; 4], header: [u8; 0x10]) -> Result<Vec<u8>, XteaError> {
151        //get the input buffer and trim any trailing zeros
152        let mut input_buffer = input_string.trim_end_matches('\0').as_bytes().to_vec();
153        let checksum = crc32fast::hash(&input_buffer);
154
155        let padding = 8 - (input_buffer.len() % 8);
156        if padding < 8 {
157            input_buffer.extend(vec![0u8; padding]);
158        }
159        let mut out_buffer = vec![0u8; input_buffer.len()];
160        let xtea = XTEA::new(&key);
161
162        let mut input_reader = Cursor::new(&input_buffer);
163        let mut output_writer = Cursor::new(&mut out_buffer);
164
165        xtea.encipher_stream::<LittleEndian, _, _>(&mut input_reader, &mut output_writer)
166            .map_err(XteaError::CipherError)?;
167
168        let mut final_buffer = Vec::new();
169        final_buffer.extend_from_slice(&header);
170
171        final_buffer
172            .write_u32::<LittleEndian>(checksum)
173            .map_err(XteaError::XteaEncodingError)?;
174
175        final_buffer.extend_from_slice(&out_buffer);
176
177        Ok(final_buffer)
178    }
179
180    pub fn encrypt_string(input_string: String, key: &[u32; 4]) -> Result<Vec<u8>, XteaError> {
181        let mut input_buffer = input_string.into_bytes();
182
183        // Pad the input buffer to make its length a multiple of 8 bytes
184        let padding = 8 - (input_buffer.len() % 8);
185        if padding < 8 {
186            input_buffer.extend(vec![0u8; padding]);
187        }
188
189        let mut out_buffer = vec![0u8; input_buffer.len()];
190        let xtea = XTEA::new(key);
191
192        let mut input_reader = Cursor::new(&input_buffer);
193        let mut output_writer = Cursor::new(&mut out_buffer);
194
195        xtea.encipher_stream::<LittleEndian, _, _>(&mut input_reader, &mut output_writer)
196            .map_err(XteaError::CipherError)?;
197
198        Ok(out_buffer)
199    }
200}