use aes::Aes256;
use cbc::cipher::{BlockDecryptMut, KeyIvInit};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use crate::error::{RarError, Result};
type Aes256CbcDec = cbc::Decryptor<Aes256>;
type HmacSha256 = Hmac<Sha256>;
#[derive(Debug, Clone)]
pub struct Rar5EncryptionHeader {
pub lg2_count: u8,
pub salt: Vec<u8>,
pub has_psw_check: bool,
pub psw_check: Vec<u8>,
pub psw_check_sum: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct DerivedKey {
pub key: [u8; 32],
pub iv: [u8; 16],
}
pub fn derive_rar5_key(password: &str, header: &Rar5EncryptionHeader) -> Result<DerivedKey> {
let iterations = 1u32 << (header.lg2_count as u32);
let password_bytes = password.as_bytes();
let mut salt_extended = header.salt.clone();
salt_extended.extend_from_slice(&[0, 0, 0, 1]);
let round_counts = [iterations, 17, 17];
let mut results: Vec<[u8; 32]> = Vec::with_capacity(3);
let mut mac = HmacSha256::new_from_slice(password_bytes)
.map_err(|e| RarError::DecryptionError(format!("HMAC init error: {e}")))?;
mac.update(&salt_extended);
let mut block: [u8; 32] = mac.finalize().into_bytes().into();
let mut final_hash = block;
for &count in &round_counts {
for _ in 1..count {
let mut mac = HmacSha256::new_from_slice(password_bytes)
.map_err(|e| RarError::DecryptionError(format!("HMAC init error: {e}")))?;
mac.update(&block);
block = mac.finalize().into_bytes().into();
for (f, b) in final_hash.iter_mut().zip(block.iter()) {
*f ^= *b;
}
}
results.push(final_hash);
}
if header.has_psw_check && header.psw_check.len() == 8 {
let mut derived_check = [0u8; 8];
for (i, &byte) in results[2].iter().enumerate() {
derived_check[i % 8] ^= byte;
}
if derived_check != header.psw_check.as_slice() {
return Err(RarError::IncorrectPassword);
}
}
let mut key = [0u8; 32];
key.copy_from_slice(&results[0]);
Ok(DerivedKey { key, iv: [0u8; 16] })
}
pub fn decrypt_rar5_headers(data: &[u8], key: &[u8; 32], iv: &[u8; 16]) -> Result<Vec<u8>> {
if data.is_empty() {
return Ok(Vec::new());
}
let aligned_len = data.len() - (data.len() % 16);
if aligned_len == 0 {
return Err(RarError::DecryptionError(
"encrypted data too short for decryption".to_string(),
));
}
let mut buf = data[..aligned_len].to_vec();
Aes256CbcDec::new(key.into(), iv.into())
.decrypt_padded_mut::<cbc::cipher::block_padding::NoPadding>(&mut buf)
.map_err(|e| RarError::DecryptionError(format!("AES-256-CBC decrypt error: {e}")))?;
Ok(buf)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn derive_key_deterministic() {
let header = Rar5EncryptionHeader {
lg2_count: 15,
salt: vec![0u8; 16],
has_psw_check: false,
psw_check: Vec::new(),
psw_check_sum: Vec::new(),
};
let key1 = derive_rar5_key("test", &header).unwrap();
let key2 = derive_rar5_key("test", &header).unwrap();
assert_eq!(key1.key, key2.key);
}
#[test]
fn derive_key_different_passwords() {
let header = Rar5EncryptionHeader {
lg2_count: 10,
salt: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
has_psw_check: false,
psw_check: Vec::new(),
psw_check_sum: Vec::new(),
};
let key1 = derive_rar5_key("password1", &header).unwrap();
let key2 = derive_rar5_key("password2", &header).unwrap();
assert_ne!(key1.key, key2.key);
}
#[test]
fn decrypt_empty_data() {
let key = [0u8; 32];
let iv = [0u8; 16];
let result = decrypt_rar5_headers(&[], &key, &iv).unwrap();
assert!(result.is_empty());
}
#[test]
fn decrypt_rejects_non_aligned_data() {
let key = [0u8; 32];
let iv = [0u8; 16];
let data = vec![0u8; 15]; let result = decrypt_rar5_headers(&data, &key, &iv);
assert!(result.is_err());
}
#[test]
fn derive_key_known_values() {
let salt = hex::decode("8246bf4a50c80674189774196e0551b3").unwrap();
let psw_check = hex::decode("c0fa7586afb0c24c").unwrap();
let header = Rar5EncryptionHeader {
lg2_count: 15,
salt,
has_psw_check: true,
psw_check,
psw_check_sum: Vec::new(),
};
let result = derive_rar5_key("U0b7258526OROQY", &header);
assert!(
result.is_ok(),
"derive_rar5_key should succeed with correct password: {result:?}"
);
}
#[test]
fn derive_key_wrong_password() {
let salt = hex::decode("8246bf4a50c80674189774196e0551b3").unwrap();
let psw_check = hex::decode("c0fa7586afb0c24c").unwrap();
let header = Rar5EncryptionHeader {
lg2_count: 15,
salt,
has_psw_check: true,
psw_check,
psw_check_sum: Vec::new(),
};
let result = derive_rar5_key("wrong_password", &header);
assert!(
result.is_err(),
"derive_rar5_key should fail with wrong password"
);
assert!(
matches!(result, Err(RarError::IncorrectPassword)),
"expected IncorrectPassword error, got: {result:?}"
);
}
#[test]
fn derive_key_no_check() {
let header = Rar5EncryptionHeader {
lg2_count: 15,
salt: vec![0xAA; 16],
has_psw_check: false,
psw_check: Vec::new(),
psw_check_sum: Vec::new(),
};
let result = derive_rar5_key("anything", &header);
assert!(
result.is_ok(),
"derive_rar5_key with no check should always succeed: {result:?}"
);
let result2 = derive_rar5_key("totally_different", &header);
assert!(result2.is_ok());
assert_ne!(result.unwrap().key, result2.unwrap().key);
}
#[test]
fn encrypt_decrypt_roundtrip() {
use aes::Aes256;
use cbc::cipher::{BlockEncryptMut, KeyIvInit};
type Aes256CbcEnc = cbc::Encryptor<Aes256>;
let key = [0x42u8; 32];
let iv = [0x13u8; 16];
let plaintext = b"hello world 1234";
let mut buf = plaintext.to_vec();
let enc = Aes256CbcEnc::new(&key.into(), &iv.into());
enc.encrypt_padded_mut::<cbc::cipher::block_padding::NoPadding>(&mut buf, 16)
.unwrap();
let decrypted = decrypt_rar5_headers(&buf, &key, &iv).unwrap();
assert_eq!(&decrypted, plaintext);
}
}