Skip to main content

rars_crypto/
rar30.rs

1use aes::cipher::{BlockCipherDecrypt, BlockCipherEncrypt, KeyInit};
2use aes::Aes128;
3use sha1::{Digest, Sha1 as FastSha1};
4use std::str;
5use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
6
7const HASH_ROUNDS: u32 = 0x40000;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum Error {
12    NonUtf8Password,
13    UnalignedInput,
14}
15
16impl std::fmt::Display for Error {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        match self {
19            Self::NonUtf8Password => f.write_str("RAR 3.x password is not UTF-8"),
20            Self::UnalignedInput => f.write_str("RAR 3.x AES input is not block aligned"),
21        }
22    }
23}
24
25impl std::error::Error for Error {}
26
27pub type Result<T> = std::result::Result<T, Error>;
28
29#[derive(Clone, ZeroizeOnDrop)]
30pub struct Rar30Cipher {
31    cipher: Aes128,
32    iv: [u8; 16],
33}
34
35impl Rar30Cipher {
36    pub fn new(password: &[u8], salt: Option<[u8; 8]>) -> Result<Self> {
37        let (mut key, iv) = derive_key_iv(password, salt)?;
38        let cipher = Aes128::new(&key.into());
39        key.zeroize();
40        Ok(Self { cipher, iv })
41    }
42
43    pub fn decrypt_in_place(&mut self, data: &mut [u8]) -> Result<()> {
44        if !data.len().is_multiple_of(16) {
45            return Err(Error::UnalignedInput);
46        }
47        for block in data.chunks_exact_mut(16) {
48            self.decrypt_block(block);
49        }
50        Ok(())
51    }
52
53    pub fn encrypt_in_place(&mut self, data: &mut [u8]) -> Result<()> {
54        if !data.len().is_multiple_of(16) {
55            return Err(Error::UnalignedInput);
56        }
57        for block in data.chunks_exact_mut(16) {
58            self.encrypt_block(block);
59        }
60        Ok(())
61    }
62
63    fn encrypt_block(&mut self, block: &mut [u8]) {
64        for (byte, iv_byte) in block.iter_mut().zip(self.iv) {
65            *byte ^= iv_byte;
66        }
67        let block: &mut [u8; 16] = block.try_into().expect("AES block size");
68        self.cipher.encrypt_block(block.into());
69        self.iv.copy_from_slice(block);
70    }
71
72    fn decrypt_block(&mut self, block: &mut [u8]) {
73        let ciphertext: [u8; 16] = block.try_into().expect("AES block size");
74        let block: &mut [u8; 16] = block.try_into().expect("AES block size");
75        self.cipher.decrypt_block(block.into());
76        for (byte, iv_byte) in block.iter_mut().zip(self.iv) {
77            *byte ^= iv_byte;
78        }
79        self.iv = ciphertext;
80    }
81}
82
83fn derive_key_iv(password: &[u8], salt: Option<[u8; 8]>) -> Result<([u8; 16], [u8; 16])> {
84    let mut raw = Zeroizing::new(Vec::with_capacity(password.len() * 2 + 8));
85    let password = str::from_utf8(password).map_err(|_| Error::NonUtf8Password)?;
86    for code_unit in password.encode_utf16() {
87        raw.extend_from_slice(&code_unit.to_le_bytes());
88    }
89    if let Some(salt) = salt {
90        raw.extend_from_slice(&salt);
91    }
92
93    // RAR 3.x mutates password/salt bytes only when the repeated KDF input
94    // crosses complete SHA-1 blocks. The stock SHA-1 path is equivalent while
95    // the password+salt material never fills a 64-byte block.
96    if raw.len() < 64 {
97        return Ok(derive_key_iv_fast(&raw));
98    }
99
100    Ok(derive_key_iv_slow(&mut raw))
101}
102
103fn derive_key_iv_slow(raw: &mut [u8]) -> ([u8; 16], [u8; 16]) {
104    let raw_size = raw.len();
105    let mut raw = Zeroizing::new(raw.to_vec());
106    raw.resize(raw_size + 64, 0);
107    let mut sha1 = FastSha1::new();
108    let mut iv = [0; 16];
109    let mut pos = 0u32;
110    for i in 0..HASH_ROUNDS {
111        sha1.update(&raw[..raw_size]);
112        let end_pos = (pos + raw_size as u32) & !(64 - 1);
113        if end_pos > pos + 64 {
114            let mut cur_pos = (pos & !(64 - 1)) + 64;
115            while cur_pos != end_pos {
116                let offset = (cur_pos - pos) as usize;
117                update_password_data_sha1(&mut raw[offset..offset + 64]);
118                cur_pos += 64;
119            }
120        }
121        pos = pos.wrapping_add(raw_size as u32);
122
123        sha1.update([
124            (i & 0xff) as u8,
125            ((i >> 8) & 0xff) as u8,
126            ((i >> 16) & 0xff) as u8,
127        ]);
128        pos = pos.wrapping_add(3);
129        if i.is_multiple_of(HASH_ROUNDS / 16) {
130            let digest = sha1.clone().finalize();
131            iv[(i / (HASH_ROUNDS / 16)) as usize] = digest[19];
132        }
133    }
134
135    let digest = sha1.finalize();
136    let mut key = [0; 16];
137    for (word_index, chunk) in digest[..16].chunks_exact(4).enumerate() {
138        key[word_index * 4..word_index * 4 + 4]
139            .copy_from_slice(&[chunk[3], chunk[2], chunk[1], chunk[0]]);
140    }
141    (key, iv)
142}
143
144fn update_password_data_sha1(data: &mut [u8]) {
145    let mut w = [0u32; 80];
146    for (i, chunk) in data.chunks_exact(4).take(16).enumerate() {
147        w[i] = u32::from_be_bytes(chunk.try_into().expect("SHA-1 word size"));
148    }
149    for i in 16..80 {
150        w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1);
151    }
152    for (i, word) in w[64..80].iter().enumerate() {
153        data[i * 4..i * 4 + 4].copy_from_slice(&word.to_le_bytes());
154    }
155}
156
157fn derive_key_iv_fast(raw: &[u8]) -> ([u8; 16], [u8; 16]) {
158    let mut sha1 = FastSha1::new();
159    let mut iv = [0; 16];
160    for i in 0..HASH_ROUNDS {
161        sha1.update(raw);
162        sha1.update([
163            (i & 0xff) as u8,
164            ((i >> 8) & 0xff) as u8,
165            ((i >> 16) & 0xff) as u8,
166        ]);
167        if i.is_multiple_of(HASH_ROUNDS / 16) {
168            let digest = sha1.clone().finalize();
169            iv[(i / (HASH_ROUNDS / 16)) as usize] = digest[19];
170        }
171    }
172
173    let digest = sha1.finalize();
174    let mut key = [0; 16];
175    for (word_index, chunk) in digest[..16].chunks_exact(4).enumerate() {
176        key[word_index * 4..word_index * 4 + 4]
177            .copy_from_slice(&[chunk[3], chunk[2], chunk[1], chunk[0]]);
178    }
179    (key, iv)
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    fn raw_kdf_material(password: &[u8], salt: Option<[u8; 8]>) -> Vec<u8> {
187        let mut raw = Vec::with_capacity(password.len() * 2 + 8);
188        let password = str::from_utf8(password).unwrap();
189        for code_unit in password.encode_utf16() {
190            raw.extend_from_slice(&code_unit.to_le_bytes());
191        }
192        if let Some(salt) = salt {
193            raw.extend_from_slice(&salt);
194        }
195        raw
196    }
197
198    #[test]
199    fn rar30_aes_encrypt_decrypt_round_trips_blocks() {
200        let salt = Some([1, 2, 3, 4, 5, 6, 7, 8]);
201        let mut data = *b"0123456789abcdefRAR AES CBC data";
202        let plain = data;
203
204        Rar30Cipher::new(b"password", salt)
205            .unwrap()
206            .encrypt_in_place(&mut data)
207            .unwrap();
208        assert_eq!(
209            data,
210            [
211                0x5e, 0x59, 0xce, 0xa1, 0x16, 0xca, 0xa2, 0x1d, 0x4d, 0xc5, 0x05, 0xeb, 0xa9, 0x3f,
212                0x7b, 0xcd, 0x0d, 0x04, 0xff, 0xea, 0x60, 0x67, 0x3d, 0xaf, 0x6a, 0x8f, 0x02, 0xb2,
213                0x03, 0xc8, 0x7d, 0xde,
214            ]
215        );
216
217        Rar30Cipher::new(b"password", salt)
218            .unwrap()
219            .decrypt_in_place(&mut data)
220            .unwrap();
221        assert_eq!(data, plain);
222    }
223
224    #[test]
225    fn rar30_aes_round_trips_with_long_password_slow_path() {
226        // Password long enough that utf-16(password) + 8-byte salt >= 64,
227        // forcing derive_key_iv to use the RAR3 password-buffer mutation path
228        // instead of derive_key_iv_fast.
229        let password = b"this-password-is-deliberately-long-enough-to-exceed-64-bytes-utf16";
230        let salt = Some(*b"longsalt");
231        let mut data = *b"0123456789abcdefRAR AES CBC data";
232        let plain = data;
233
234        Rar30Cipher::new(password, salt)
235            .unwrap()
236            .encrypt_in_place(&mut data)
237            .unwrap();
238        assert_eq!(
239            data,
240            [
241                0xb9, 0xa7, 0xac, 0x4b, 0x81, 0x0a, 0x5c, 0xf1, 0x6e, 0xd4, 0x5a, 0x4c, 0xbc, 0x1e,
242                0x2e, 0xef, 0x53, 0x7b, 0x89, 0x63, 0x7a, 0xc5, 0x7a, 0x1e, 0xfc, 0x43, 0x3c, 0x18,
243                0xea, 0xfd, 0x54, 0xed,
244            ]
245        );
246
247        Rar30Cipher::new(password, salt)
248            .unwrap()
249            .decrypt_in_place(&mut data)
250            .unwrap();
251        assert_eq!(data, plain);
252    }
253
254    #[test]
255    fn rar30_aes_rejects_partial_tail() {
256        let mut data = *b"partial block!!";
257
258        assert_eq!(
259            Rar30Cipher::new(b"password", None)
260                .unwrap()
261                .encrypt_in_place(&mut data),
262            Err(Error::UnalignedInput)
263        );
264        assert_eq!(
265            Rar30Cipher::new(b"password", None)
266                .unwrap()
267                .decrypt_in_place(&mut data),
268            Err(Error::UnalignedInput)
269        );
270    }
271
272    #[test]
273    fn rejects_non_utf8_passwords() {
274        assert!(matches!(
275            Rar30Cipher::new(b"\xffpassword", None),
276            Err(Error::NonUtf8Password)
277        ));
278    }
279
280    #[test]
281    fn rar30_fast_kdf_matches_reference_path_for_short_material() {
282        for (password, salt) in [
283            (b"".as_slice(), None),
284            (b"password".as_slice(), Some(*b"rarsalt!")),
285            ("páss".as_bytes(), Some([1, 2, 3, 4, 5, 6, 7, 8])),
286        ] {
287            let raw = raw_kdf_material(password, salt);
288            assert!(
289                raw.len() < 64,
290                "case should exercise the fast-path precondition"
291            );
292
293            let fast = derive_key_iv_fast(&raw);
294            let mut reference_raw = raw.clone();
295            let reference = derive_key_iv_slow(&mut reference_raw);
296
297            assert_eq!(fast, reference);
298        }
299    }
300}