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 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 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}