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            // Validate psw_check_sum = SHA-256(psw_check)[0..4]
106            use sha2::{Digest, Sha256};
107            let hash = Sha256::digest(check);
108            if hash[..SIZE_PSWCHECK_CSUM] != sum {
109                return Err(super::CryptoError::InvalidHeader);
110            }
111
112            (Some(check), Some(sum))
113        } else {
114            (None, None)
115        };
116
117        Ok(Self {
118            version,
119            flags,
120            lg2_count,
121            salt,
122            init_v,
123            psw_check,
124            psw_check_sum,
125        })
126    }
127}
128
129/// RAR5 cryptographic operations.
130#[derive(Clone, Debug)]
131pub struct Rar5Crypto {
132    /// Derived AES-256 key
133    key: [u8; 32],
134    /// Password check value (XOR of hash key iterations)
135    psw_check_value: [u8; 32],
136}
137
138impl Rar5Crypto {
139    /// Derive key from password using PBKDF2-HMAC-SHA256.
140    ///
141    /// RAR5 derives these values using PBKDF2-HMAC-SHA256:
142    /// 1. Key (32 bytes) - for AES-256 encryption (at `iterations`)
143    /// 2. Password check (32 bytes) - for password verification (at `iterations + 32`)
144    ///
145    /// Note: RAR5 spec also defines a hash key (at `iterations + 16`) for MAC
146    /// checksums, but this implementation does not compute it since we don't
147    /// verify header MACs.
148    pub fn derive_key(password: &str, salt: &[u8; SIZE_SALT50], lg2_count: u8) -> Self {
149        // Clamp to valid range to prevent shift overflow in release builds.
150        // CRYPT5_KDF_LG2_COUNT_MAX is 24 per RAR5 spec; values above 31 would
151        // wrap the u32 shift, undermining key derivation security.
152        let lg2_count = lg2_count.min(CRYPT5_KDF_LG2_COUNT_MAX as u8);
153        let iterations = 1u32 << lg2_count;
154
155        // Derive key material
156        // RAR uses PBKDF2 at different iteration counts for different values
157
158        let mut key = [0u8; 32];
159        pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, iterations, &mut key);
160
161        // Password check value - computed at iterations + 32
162        // This is used to verify the password without decrypting
163        let mut psw_check_value = [0u8; 32];
164        pbkdf2_hmac::<Sha256>(
165            password.as_bytes(),
166            salt,
167            iterations + 32,
168            &mut psw_check_value,
169        );
170
171        Self {
172            key,
173            psw_check_value,
174        }
175    }
176
177    /// Verify password using the stored check value.
178    pub fn verify_password(&self, expected: &[u8; SIZE_PSWCHECK]) -> bool {
179        // The check value is XOR of all bytes in psw_check_value,
180        // folded into 8 bytes
181        let mut check = [0u8; SIZE_PSWCHECK];
182        for (i, &byte) in self.psw_check_value.iter().enumerate() {
183            check[i % SIZE_PSWCHECK] ^= byte;
184        }
185        // Constant-time comparison to prevent timing side-channel attacks
186        let mut diff = 0u8;
187        for (a, b) in check.iter().zip(expected.iter()) {
188            diff |= a ^ b;
189        }
190        diff == 0
191    }
192
193    /// Decrypt data in-place using AES-256-CBC.
194    pub fn decrypt(
195        &self,
196        iv: &[u8; SIZE_INITV],
197        data: &mut [u8],
198    ) -> Result<(), super::CryptoError> {
199        // Data must be a multiple of block size
200        if data.len() % CRYPT_BLOCK_SIZE != 0 {
201            return Err(super::CryptoError::DecryptionFailed);
202        }
203
204        let decryptor = Aes256CbcDec::new_from_slices(&self.key, iv)
205            .map_err(|_| super::CryptoError::DecryptionFailed)?;
206
207        decryptor
208            .decrypt_padded_mut::<cbc::cipher::block_padding::NoPadding>(data)
209            .map_err(|_| super::CryptoError::DecryptionFailed)?;
210
211        Ok(())
212    }
213
214    /// Decrypt data to a new buffer.
215    pub fn decrypt_to_vec(
216        &self,
217        iv: &[u8; SIZE_INITV],
218        data: &[u8],
219    ) -> Result<Vec<u8>, super::CryptoError> {
220        let mut output = data.to_vec();
221        self.decrypt(iv, &mut output)?;
222        Ok(output)
223    }
224}
225
226impl Drop for Rar5Crypto {
227    fn drop(&mut self) {
228        // Zero sensitive key material to reduce exposure window.
229        // Use write_volatile to prevent the compiler from optimizing this out.
230        for byte in &mut self.key {
231            unsafe { std::ptr::write_volatile(byte, 0) };
232        }
233        for byte in &mut self.psw_check_value {
234            unsafe { std::ptr::write_volatile(byte, 0) };
235        }
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn test_derive_key() {
245        // Test that key derivation works
246        let salt = [0u8; SIZE_SALT50];
247        let crypto = Rar5Crypto::derive_key("password", &salt, 15);
248
249        // Just verify it produces deterministic output
250        let crypto2 = Rar5Crypto::derive_key("password", &salt, 15);
251        assert_eq!(crypto.key, crypto2.key);
252        assert_eq!(crypto.psw_check_value, crypto2.psw_check_value);
253
254        // Different password should produce different key
255        let crypto3 = Rar5Crypto::derive_key("different", &salt, 15);
256        assert_ne!(crypto.key, crypto3.key);
257    }
258
259    #[test]
260    fn test_parse_encryption_info() {
261        // Minimal header: version=0, flags=0, lg2_count=15, salt, iv
262        let mut data = vec![0u8; 35];
263        data[0] = 0; // version
264        data[1] = 0; // flags
265        data[2] = 15; // lg2_count
266                      // salt and iv are zeros
267
268        let info = Rar5EncryptionInfo::parse(&data).unwrap();
269        assert_eq!(info.version, 0);
270        assert_eq!(info.flags, 0);
271        assert_eq!(info.lg2_count, 15);
272        assert!(info.psw_check.is_none());
273    }
274
275    #[test]
276    fn test_parse_encryption_info_with_check() {
277        use sha2::{Digest, Sha256};
278
279        // Header with password check: version=0, flags=1, lg2_count=15, salt, iv, check, sum
280        let mut data = vec![0u8; 47];
281        data[0] = 0; // version
282        data[1] = 1; // flags - password check present
283        data[2] = 15; // lg2_count
284                      // Fill check value (bytes 35..43)
285        for i in 35..43 {
286            data[i] = i as u8;
287        }
288        // Compute correct psw_check_sum = SHA-256(psw_check)[0..4]
289        let hash = Sha256::digest(&data[35..43]);
290        data[43..47].copy_from_slice(&hash[..4]);
291
292        let info = Rar5EncryptionInfo::parse(&data).unwrap();
293        assert_eq!(info.flags, 1);
294        assert!(info.psw_check.is_some());
295        assert!(info.psw_check_sum.is_some());
296    }
297
298    #[test]
299    fn test_decrypt_encrypted_rar5() {
300        use crate::parsing::rar5::file_header::Rar5FileHeaderParser;
301        use crate::parsing::rar5::VintReader;
302
303        // Read the encrypted RAR5 file (created with rar -ma5 -p"testpass")
304        let data = std::fs::read("__fixtures__/encrypted/rar5-encrypted-v5.rar").unwrap();
305
306        // Skip the 8-byte RAR5 signature
307        let _after_sig = &data[8..];
308
309        // Find the file header (type 2)
310        let mut pos = 8; // After signature
311        loop {
312            assert!(pos + 7 <= data.len(), "Could not find file header");
313
314            // Read header: CRC32 (4) + header_size (vint) + header_type (vint)
315            let mut reader = VintReader::new(&data[pos + 4..]);
316            let header_size = reader.read().unwrap();
317            let header_type = reader.read().unwrap();
318
319            if header_type == 2 {
320                // File header found
321                let (file_header, _) = Rar5FileHeaderParser::parse(&data[pos..]).unwrap();
322
323                if file_header.is_encrypted() {
324                    let enc_data = file_header.encryption_info().unwrap();
325
326                    let enc_info = Rar5EncryptionInfo::parse(enc_data).unwrap();
327
328                    // Derive key with correct password
329                    let crypto =
330                        Rar5Crypto::derive_key("testpass", &enc_info.salt, enc_info.lg2_count);
331
332                    // Verify password if check value is present
333                    if let Some(ref check) = enc_info.psw_check {
334                        let valid = crypto.verify_password(check);
335                        assert!(valid, "Password verification failed");
336                    }
337
338                    // Now decrypt the actual file data
339                    // The packed data follows the file header
340                    let header_total_size = 4 + 1 + header_size as usize; // CRC + size vint + content
341                    let data_start = pos + header_total_size;
342                    let data_end = data_start + file_header.packed_size as usize;
343
344                    if data_end <= data.len() {
345                        let encrypted_data = &data[data_start..data_end];
346
347                        // Decrypt the data
348                        let decrypted = crypto
349                            .decrypt_to_vec(&enc_info.init_v, encrypted_data)
350                            .unwrap();
351
352                        // The decrypted data should be compressed - we can't verify the content
353                        // directly without decompressing, but we can verify decryption succeeded
354                        assert_eq!(decrypted.len(), encrypted_data.len());
355
356                        // For stored files (method 0), we could verify content directly
357                        // For compressed files, the decrypted data is still compressed
358                    }
359                }
360                break;
361            }
362
363            // Move to next header: 4 bytes CRC + size vint length + header content
364            let size_vint_len = {
365                let mut r = VintReader::new(&data[pos + 4..]);
366                r.read().unwrap();
367                r.position()
368            };
369            pos += 4 + size_vint_len + header_size as usize;
370        }
371    }
372
373    #[test]
374    fn test_decrypt_stored_file() {
375        use crate::parsing::rar5::file_header::Rar5FileHeaderParser;
376        use crate::parsing::rar5::VintReader;
377
378        // Read the stored encrypted RAR5 file (created with rar -ma5 -m0 -p"testpass")
379        let data = std::fs::read("__fixtures__/encrypted/rar5-encrypted-stored.rar").unwrap();
380
381        // Find the file header (type 2)
382        let mut pos = 8; // After signature
383        loop {
384            assert!(pos + 7 <= data.len(), "Could not find file header");
385
386            let mut reader = VintReader::new(&data[pos + 4..]);
387            let header_size = reader.read().unwrap();
388            let header_type = reader.read().unwrap();
389
390            if header_type == 2 {
391                let (file_header, consumed) = Rar5FileHeaderParser::parse(&data[pos..]).unwrap();
392
393                assert!(file_header.is_encrypted());
394                assert!(
395                    file_header.is_stored(),
396                    "File should be stored (uncompressed)"
397                );
398
399                let enc_data = file_header.encryption_info().unwrap();
400                let enc_info = Rar5EncryptionInfo::parse(enc_data).unwrap();
401
402                let crypto = Rar5Crypto::derive_key("testpass", &enc_info.salt, enc_info.lg2_count);
403
404                // Verify password
405                if let Some(ref check) = enc_info.psw_check {
406                    assert!(
407                        crypto.verify_password(check),
408                        "Password verification failed"
409                    );
410                }
411
412                // Decrypt the file data
413                let data_start = pos + consumed;
414                let data_end = data_start + file_header.packed_size as usize;
415                let encrypted_data = &data[data_start..data_end];
416
417                let decrypted = crypto
418                    .decrypt_to_vec(&enc_info.init_v, encrypted_data)
419                    .unwrap();
420
421                // For stored files, decrypted data IS the original content (with padding)
422                // The original file is "Hello, encrypted world!\n" (24 bytes)
423                // Padded to 32 bytes (next multiple of 16)
424                let expected = b"Hello, encrypted world!\n";
425                assert!(
426                    decrypted.starts_with(expected),
427                    "Decrypted content doesn't match. Got: {:?}",
428                    String::from_utf8_lossy(&decrypted[..expected.len().min(decrypted.len())])
429                );
430                break;
431            }
432
433            let size_vint_len = {
434                let mut r = VintReader::new(&data[pos + 4..]);
435                r.read().unwrap();
436                r.position()
437            };
438            pos += 4 + size_vint_len + header_size as usize;
439        }
440    }
441}