Skip to main content

rar_stream/crypto/
rar5.rs

1//! RAR5 encryption implementation.
2//!
3//! RAR5 uses:
4//! - AES-256-CBC for encryption
5//! - PBKDF2-HMAC-SHA256 for key derivation
6//! - 16-byte salt
7//! - Configurable iteration count (2^lg2_count)
8//! - 8-byte password check value for fast verification
9
10use aes::Aes256;
11use cbc::cipher::{BlockDecryptMut, KeyIvInit};
12use pbkdf2::pbkdf2_hmac;
13use sha2::Sha256;
14
15type Aes256CbcDec = cbc::Decryptor<Aes256>;
16
17/// Size constants for RAR5 encryption.
18pub const SIZE_SALT50: usize = 16;
19pub const SIZE_INITV: usize = 16;
20pub const SIZE_PSWCHECK: usize = 8;
21pub const SIZE_PSWCHECK_CSUM: usize = 4;
22pub const CRYPT_BLOCK_SIZE: usize = 16;
23
24/// Default PBKDF2 iteration count (log2).
25/// Actual iterations = 2^15 = 32768
26#[allow(dead_code)]
27pub const CRYPT5_KDF_LG2_COUNT: u32 = 15;
28
29/// Maximum allowed PBKDF2 iteration count (log2).
30#[allow(dead_code)]
31pub const CRYPT5_KDF_LG2_COUNT_MAX: u32 = 24;
32
33/// RAR5 encryption information parsed from file header.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Rar5EncryptionInfo {
36    /// Encryption version (must be 0)
37    pub version: u8,
38    /// Flags (0x01 = password check present, 0x02 = use MAC for checksums)
39    pub flags: u8,
40    /// Log2 of PBKDF2 iteration count
41    pub lg2_count: u8,
42    /// 16-byte salt
43    pub salt: [u8; SIZE_SALT50],
44    /// 16-byte initialization vector
45    pub init_v: [u8; SIZE_INITV],
46    /// Optional 8-byte password check value
47    pub psw_check: Option<[u8; SIZE_PSWCHECK]>,
48    /// Optional 4-byte password check sum
49    pub psw_check_sum: Option<[u8; SIZE_PSWCHECK_CSUM]>,
50}
51
52impl Rar5EncryptionInfo {
53    /// Parse encryption info from extra data.
54    /// Format:
55    /// - vint: version
56    /// - vint: flags
57    /// - 1 byte: lg2_count
58    /// - 16 bytes: salt
59    /// - 16 bytes: init_v
60    /// - if flags & 0x01:
61    ///   - 8 bytes: psw_check
62    ///   - 4 bytes: psw_check_sum (SHA-256 of psw_check, first 4 bytes)
63    pub fn parse(data: &[u8]) -> Result<Self, super::CryptoError> {
64        use crate::parsing::rar5::VintReader;
65
66        let mut reader = VintReader::new(data);
67
68        let version = reader.read().ok_or(super::CryptoError::InvalidHeader)? as u8;
69        if version != 0 {
70            return Err(super::CryptoError::UnsupportedVersion(version));
71        }
72
73        let flags = reader.read().ok_or(super::CryptoError::InvalidHeader)? as u8;
74
75        let lg2_bytes = reader
76            .read_bytes(1)
77            .ok_or(super::CryptoError::InvalidHeader)?;
78        let lg2_count = lg2_bytes[0];
79
80        let salt_bytes = reader
81            .read_bytes(SIZE_SALT50)
82            .ok_or(super::CryptoError::InvalidHeader)?;
83        let mut salt = [0u8; SIZE_SALT50];
84        salt.copy_from_slice(salt_bytes);
85
86        let iv_bytes = reader
87            .read_bytes(SIZE_INITV)
88            .ok_or(super::CryptoError::InvalidHeader)?;
89        let mut init_v = [0u8; SIZE_INITV];
90        init_v.copy_from_slice(iv_bytes);
91
92        let (psw_check, psw_check_sum) = if flags & 0x01 != 0 {
93            let check_bytes = reader
94                .read_bytes(SIZE_PSWCHECK)
95                .ok_or(super::CryptoError::InvalidHeader)?;
96            let mut check = [0u8; SIZE_PSWCHECK];
97            check.copy_from_slice(check_bytes);
98
99            let sum_bytes = reader
100                .read_bytes(SIZE_PSWCHECK_CSUM)
101                .ok_or(super::CryptoError::InvalidHeader)?;
102            let mut sum = [0u8; SIZE_PSWCHECK_CSUM];
103            sum.copy_from_slice(sum_bytes);
104
105            (Some(check), Some(sum))
106        } else {
107            (None, None)
108        };
109
110        Ok(Self {
111            version,
112            flags,
113            lg2_count,
114            salt,
115            init_v,
116            psw_check,
117            psw_check_sum,
118        })
119    }
120}
121
122/// RAR5 cryptographic operations.
123#[derive(Clone, Debug)]
124pub struct Rar5Crypto {
125    /// Derived AES-256 key
126    key: [u8; 32],
127    /// Password check value (XOR of hash key iterations)
128    psw_check_value: [u8; 32],
129}
130
131impl Rar5Crypto {
132    /// Derive key from password using PBKDF2-HMAC-SHA256.
133    ///
134    /// RAR5 derives 3 values:
135    /// 1. Key (32 bytes) - for AES-256 encryption
136    /// 2. Hash key (32 bytes) - for MAC checksums (iterations + 16)
137    /// 3. Password check (32 bytes) - for password verification (iterations + 32)
138    pub fn derive_key(password: &str, salt: &[u8; SIZE_SALT50], lg2_count: u8) -> Self {
139        let iterations = 1u32 << lg2_count;
140
141        // Derive key material: 32 bytes key + 32 bytes hash_key + 32 bytes psw_check
142        // RAR uses a modified PBKDF2 that outputs these at different iteration counts
143        // For simplicity, we compute the standard PBKDF2 and then the additional values
144
145        let mut key = [0u8; 32];
146        pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, iterations, &mut key);
147
148        // Password check value - computed at iterations + 32
149        // This is used to verify the password without decrypting
150        let mut psw_check_value = [0u8; 32];
151        pbkdf2_hmac::<Sha256>(
152            password.as_bytes(),
153            salt,
154            iterations + 32,
155            &mut psw_check_value,
156        );
157
158        Self {
159            key,
160            psw_check_value,
161        }
162    }
163
164    /// Verify password using the stored check value.
165    pub fn verify_password(&self, expected: &[u8; SIZE_PSWCHECK]) -> bool {
166        // The check value is XOR of all bytes in psw_check_value,
167        // folded into 8 bytes
168        let mut check = [0u8; SIZE_PSWCHECK];
169        for (i, &byte) in self.psw_check_value.iter().enumerate() {
170            check[i % SIZE_PSWCHECK] ^= byte;
171        }
172        check == *expected
173    }
174
175    /// Decrypt data in-place using AES-256-CBC.
176    pub fn decrypt(
177        &self,
178        iv: &[u8; SIZE_INITV],
179        data: &mut [u8],
180    ) -> Result<(), super::CryptoError> {
181        // Data must be a multiple of block size
182        if data.len() % CRYPT_BLOCK_SIZE != 0 {
183            return Err(super::CryptoError::DecryptionFailed);
184        }
185
186        let decryptor = Aes256CbcDec::new_from_slices(&self.key, iv)
187            .map_err(|_| super::CryptoError::DecryptionFailed)?;
188
189        decryptor
190            .decrypt_padded_mut::<cbc::cipher::block_padding::NoPadding>(data)
191            .map_err(|_| super::CryptoError::DecryptionFailed)?;
192
193        Ok(())
194    }
195
196    /// Decrypt data to a new buffer.
197    pub fn decrypt_to_vec(
198        &self,
199        iv: &[u8; SIZE_INITV],
200        data: &[u8],
201    ) -> Result<Vec<u8>, super::CryptoError> {
202        let mut output = data.to_vec();
203        self.decrypt(iv, &mut output)?;
204        Ok(output)
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_derive_key() {
214        // Test that key derivation works
215        let salt = [0u8; SIZE_SALT50];
216        let crypto = Rar5Crypto::derive_key("password", &salt, 15);
217
218        // Just verify it produces deterministic output
219        let crypto2 = Rar5Crypto::derive_key("password", &salt, 15);
220        assert_eq!(crypto.key, crypto2.key);
221        assert_eq!(crypto.psw_check_value, crypto2.psw_check_value);
222
223        // Different password should produce different key
224        let crypto3 = Rar5Crypto::derive_key("different", &salt, 15);
225        assert_ne!(crypto.key, crypto3.key);
226    }
227
228    #[test]
229    fn test_parse_encryption_info() {
230        // Minimal header: version=0, flags=0, lg2_count=15, salt, iv
231        let mut data = vec![0u8; 35];
232        data[0] = 0; // version
233        data[1] = 0; // flags
234        data[2] = 15; // lg2_count
235                      // salt and iv are zeros
236
237        let info = Rar5EncryptionInfo::parse(&data).unwrap();
238        assert_eq!(info.version, 0);
239        assert_eq!(info.flags, 0);
240        assert_eq!(info.lg2_count, 15);
241        assert!(info.psw_check.is_none());
242    }
243
244    #[test]
245    fn test_parse_encryption_info_with_check() {
246        // Header with password check: version=0, flags=1, lg2_count=15, salt, iv, check, sum
247        let mut data = vec![0u8; 47];
248        data[0] = 0; // version
249        data[1] = 1; // flags - password check present
250        data[2] = 15; // lg2_count
251                      // Fill check value
252        for i in 35..43 {
253            data[i] = i as u8;
254        }
255
256        let info = Rar5EncryptionInfo::parse(&data).unwrap();
257        assert_eq!(info.flags, 1);
258        assert!(info.psw_check.is_some());
259        assert!(info.psw_check_sum.is_some());
260    }
261
262    #[test]
263    fn test_decrypt_encrypted_rar5() {
264        use crate::parsing::rar5::file_header::Rar5FileHeaderParser;
265        use crate::parsing::rar5::VintReader;
266
267        // Read the encrypted RAR5 file (created with rar -ma5 -p"testpass")
268        let data = std::fs::read("__fixtures__/encrypted/rar5-encrypted-v5.rar").unwrap();
269
270        // Skip the 8-byte RAR5 signature
271        let _after_sig = &data[8..];
272
273        // Find the file header (type 2)
274        let mut pos = 8; // After signature
275        loop {
276            assert!(pos + 7 <= data.len(), "Could not find file header");
277
278            // Read header: CRC32 (4) + header_size (vint) + header_type (vint)
279            let mut reader = VintReader::new(&data[pos + 4..]);
280            let header_size = reader.read().unwrap();
281            let header_type = reader.read().unwrap();
282
283            if header_type == 2 {
284                // File header found
285                let (file_header, _) = Rar5FileHeaderParser::parse(&data[pos..]).unwrap();
286
287                if file_header.is_encrypted() {
288                    let enc_data = file_header.encryption_info().unwrap();
289
290                    let enc_info = Rar5EncryptionInfo::parse(enc_data).unwrap();
291
292                    // Derive key with correct password
293                    let crypto =
294                        Rar5Crypto::derive_key("testpass", &enc_info.salt, enc_info.lg2_count);
295
296                    // Verify password if check value is present
297                    if let Some(ref check) = enc_info.psw_check {
298                        let valid = crypto.verify_password(check);
299                        assert!(valid, "Password verification failed");
300                    }
301
302                    // Now decrypt the actual file data
303                    // The packed data follows the file header
304                    let header_total_size = 4 + 1 + header_size as usize; // CRC + size vint + content
305                    let data_start = pos + header_total_size;
306                    let data_end = data_start + file_header.packed_size as usize;
307
308                    if data_end <= data.len() {
309                        let encrypted_data = &data[data_start..data_end];
310
311                        // Decrypt the data
312                        let decrypted = crypto
313                            .decrypt_to_vec(&enc_info.init_v, encrypted_data)
314                            .unwrap();
315
316                        // The decrypted data should be compressed - we can't verify the content
317                        // directly without decompressing, but we can verify decryption succeeded
318                        assert_eq!(decrypted.len(), encrypted_data.len());
319
320                        // For stored files (method 0), we could verify content directly
321                        // For compressed files, the decrypted data is still compressed
322                    }
323                }
324                break;
325            }
326
327            // Move to next header: 4 bytes CRC + size vint length + header content
328            let size_vint_len = {
329                let mut r = VintReader::new(&data[pos + 4..]);
330                r.read().unwrap();
331                r.position()
332            };
333            pos += 4 + size_vint_len + header_size as usize;
334        }
335    }
336
337    #[test]
338    fn test_decrypt_stored_file() {
339        use crate::parsing::rar5::file_header::Rar5FileHeaderParser;
340        use crate::parsing::rar5::VintReader;
341
342        // Read the stored encrypted RAR5 file (created with rar -ma5 -m0 -p"testpass")
343        let data = std::fs::read("__fixtures__/encrypted/rar5-encrypted-stored.rar").unwrap();
344
345        // Find the file header (type 2)
346        let mut pos = 8; // After signature
347        loop {
348            assert!(pos + 7 <= data.len(), "Could not find file header");
349
350            let mut reader = VintReader::new(&data[pos + 4..]);
351            let header_size = reader.read().unwrap();
352            let header_type = reader.read().unwrap();
353
354            if header_type == 2 {
355                let (file_header, consumed) = Rar5FileHeaderParser::parse(&data[pos..]).unwrap();
356
357                assert!(file_header.is_encrypted());
358                assert!(
359                    file_header.is_stored(),
360                    "File should be stored (uncompressed)"
361                );
362
363                let enc_data = file_header.encryption_info().unwrap();
364                let enc_info = Rar5EncryptionInfo::parse(enc_data).unwrap();
365
366                let crypto = Rar5Crypto::derive_key("testpass", &enc_info.salt, enc_info.lg2_count);
367
368                // Verify password
369                if let Some(ref check) = enc_info.psw_check {
370                    assert!(
371                        crypto.verify_password(check),
372                        "Password verification failed"
373                    );
374                }
375
376                // Decrypt the file data
377                let data_start = pos + consumed;
378                let data_end = data_start + file_header.packed_size as usize;
379                let encrypted_data = &data[data_start..data_end];
380
381                let decrypted = crypto
382                    .decrypt_to_vec(&enc_info.init_v, encrypted_data)
383                    .unwrap();
384
385                // For stored files, decrypted data IS the original content (with padding)
386                // The original file is "Hello, encrypted world!\n" (24 bytes)
387                // Padded to 32 bytes (next multiple of 16)
388                let expected = b"Hello, encrypted world!\n";
389                assert!(
390                    decrypted.starts_with(expected),
391                    "Decrypted content doesn't match. Got: {:?}",
392                    String::from_utf8_lossy(&decrypted[..expected.len().min(decrypted.len())])
393                );
394                break;
395            }
396
397            let size_vint_len = {
398                let mut r = VintReader::new(&data[pos + 4..]);
399                r.read().unwrap();
400                r.position()
401            };
402            pos += 4 + size_vint_len + header_size as usize;
403        }
404    }
405}