use rand::Rng;
use sha1::{Digest, Sha1};
use crate::error::{Error, Result};
use crate::ppt97::record::{
parse_persist_directory, parse_record_header, read_u32_le, write_u32_le, RT_CURRENT_USER_ATOM,
RT_PERSIST_DIRECTORY_ATOM, RT_USER_EDIT_ATOM,
};
pub const KEY_SIZE_BITS: u32 = 128;
pub const SALT_SIZE: usize = 16;
pub const VERIFIER_SIZE: usize = 16;
pub const SHA1_SIZE: usize = 20;
pub const RT_CRYPT_SESSION10_CONTAINER: u16 = 0x2F14;
pub const HEADER_TOKEN_ENCRYPTED: u32 = 0xF3D1C4DF;
pub const CSP_NAME: &str = "Microsoft Enhanced Cryptographic Provider v1.0\0";
#[derive(Debug)]
struct Rc4 {
s: [u8; 256],
i: usize,
j: usize,
}
impl Rc4 {
#[allow(clippy::needless_range_loop)]
fn new(key: &[u8]) -> Self {
let mut s = [0u8; 256];
for i in 0..256 {
s[i] = i as u8;
}
let mut j = 0;
for i in 0..256 {
j = (j + s[i] as usize + key[i % key.len()] as usize) % 256;
s.swap(i, j);
}
Self { s, i: 0, j: 0 }
}
fn process(&mut self, data: &mut [u8]) {
for byte in data {
self.i = (self.i + 1) % 256;
self.j = (self.j + self.s[self.i] as usize) % 256;
self.s.swap(self.i, self.j);
let k = self.s[(self.s[self.i] as usize + self.s[self.j] as usize) % 256];
*byte ^= k;
}
}
}
pub fn make_key(password: &str, salt: &[u8], key_bits: u32, block: u32) -> Vec<u8> {
let password_utf16le: Vec<u8> = password
.encode_utf16()
.flat_map(|c| c.to_le_bytes())
.collect();
let mut hasher = Sha1::new();
hasher.update(salt);
hasher.update(&password_utf16le);
let h0 = hasher.finalize();
let mut hasher = Sha1::new();
hasher.update(h0);
hasher.update(block.to_le_bytes());
let hfinal = hasher.finalize();
let key_bytes = key_bits as usize / 8;
if key_bits == 40 {
let mut key = hfinal[..5].to_vec();
key.resize(16, 0);
key
} else {
hfinal[..key_bytes].to_vec()
}
}
pub fn encrypt_persist_object(
password: &str,
salt: &[u8],
key_bits: u32,
data: &mut [u8],
persist_id: u32,
) -> Vec<u8> {
let total_len = data.len();
let blocksize = key_bits as usize * (total_len / key_bits as usize + 1);
let mut result = Vec::with_capacity(total_len);
let mut offset = 0;
let mut block = persist_id;
while offset < total_len {
let end = std::cmp::min(offset + blocksize, total_len);
let key = make_key(password, salt, key_bits, block);
let mut rc4 = Rc4::new(&key);
let mut chunk = data[offset..end].to_vec();
rc4.process(&mut chunk);
result.extend_from_slice(&chunk);
offset = end;
block += 1;
}
result
}
pub fn build_crypt_session10_container(
salt: &[u8],
encrypted_verifier: &[u8],
encrypted_verifier_hash: &[u8],
key_bits: u32,
) -> Vec<u8> {
let mut version_info = Vec::new();
version_info.extend_from_slice(&4u16.to_le_bytes());
version_info.extend_from_slice(&2u16.to_le_bytes());
let outer_flags: u32 = 0x0000000C;
let mut header = Vec::new();
header.extend_from_slice(&outer_flags.to_le_bytes()); header.extend_from_slice(&0u32.to_le_bytes()); header.extend_from_slice(&0x00006801u32.to_le_bytes()); header.extend_from_slice(&0x00008004u32.to_le_bytes()); header.extend_from_slice(&key_bits.to_le_bytes()); header.extend_from_slice(&0x00000001u32.to_le_bytes()); header.extend_from_slice(&0u32.to_le_bytes()); header.extend_from_slice(&0u32.to_le_bytes()); let csp_name_utf16: Vec<u8> = CSP_NAME
.encode_utf16()
.flat_map(|c| c.to_le_bytes())
.collect();
header.extend_from_slice(&csp_name_utf16);
let mut verifier = Vec::new();
verifier.extend_from_slice(&(SALT_SIZE as u32).to_le_bytes()); verifier.extend_from_slice(salt); verifier.extend_from_slice(encrypted_verifier); verifier.extend_from_slice(&(SHA1_SIZE as u32).to_le_bytes()); verifier.extend_from_slice(encrypted_verifier_hash);
let mut data = Vec::new();
data.extend_from_slice(&version_info);
data.extend_from_slice(&outer_flags.to_le_bytes()); data.extend_from_slice(&(header.len() as u32).to_le_bytes()); data.extend_from_slice(&header);
data.extend_from_slice(&verifier);
let mut result = Vec::new();
let ver_inst: u16 = 0xF; result.extend_from_slice(&ver_inst.to_le_bytes());
result.extend_from_slice(&RT_CRYPT_SESSION10_CONTAINER.to_le_bytes());
result.extend_from_slice(&(data.len() as u32).to_le_bytes());
result.extend_from_slice(&data);
result
}
pub fn reorder_persist_objects(
#[allow(clippy::ptr_arg)] ppt_data: &mut Vec<u8>,
persist_entries: &[(u32, u32)],
pd_offset: usize,
) -> Result<Vec<(u32, u32)>> {
let mut objects: Vec<(u32, Vec<u8>)> = Vec::new();
for (pid, offset) in persist_entries {
let offset = *offset as usize;
let (_, _, rec_type, rec_len) = parse_record_header(ppt_data, offset)?;
if rec_type == RT_USER_EDIT_ATOM || rec_type == RT_PERSIST_DIRECTORY_ATOM {
continue;
}
let total_len = 8 + rec_len as usize;
if offset + total_len > ppt_data.len() {
return Err(Error::ppt97(format!(
"reorder_persist_objects: persist {} (offset {}, len {}) out of range",
pid, offset, total_len
)));
}
let data = ppt_data[offset..offset + total_len].to_vec();
objects.push((*pid, data));
}
objects.sort_by_key(|(pid, _)| *pid);
let min_offset = persist_entries
.iter()
.map(|(_, off)| *off)
.min()
.unwrap_or(0) as usize;
let mut new_offsets: std::collections::HashMap<u32, u32> = std::collections::HashMap::new();
let mut write_pos = min_offset;
for (pid, data) in &objects {
if write_pos + data.len() > ppt_data.len() {
return Err(Error::ppt97(format!(
"reorder_persist_objects: write_pos {} + len {} out of range",
write_pos,
data.len()
)));
}
ppt_data[write_pos..write_pos + data.len()].copy_from_slice(data);
new_offsets.insert(*pid, write_pos as u32);
write_pos += data.len();
}
let (_, _, _, pd_rec_len) = parse_record_header(ppt_data, pd_offset)?;
let pd_data_end = pd_offset + 8 + pd_rec_len as usize;
let mut pos = pd_offset + 8;
while pos + 4 <= pd_data_end {
let entry = read_u32_le(ppt_data, pos)?;
let persist_id = entry & 0xFFFFF;
let c_persist = (entry >> 20) & 0xFFF;
pos += 4;
for j in 0..c_persist {
let pid = persist_id + j;
if let Some(&new_offset) = new_offsets.get(&pid) {
write_u32_le(ppt_data, pos, new_offset)?;
}
pos += 4;
}
}
let new_entries: Vec<(u32, u32)> = persist_entries
.iter()
.map(|(pid, old_off)| {
let new_off = new_offsets.get(pid).copied().unwrap_or(*old_off);
(*pid, new_off)
})
.collect();
Ok(new_entries)
}
pub fn encrypt_ppt_stream(
#[allow(clippy::ptr_arg)] ppt_data: &mut Vec<u8>,
#[allow(clippy::ptr_arg)] cu_data: &mut Vec<u8>,
password: &str,
) -> Result<()> {
let (_, _, cu_type, _) = parse_record_header(cu_data, 0)?;
if cu_type != RT_CURRENT_USER_ATOM {
return Err(Error::ppt97(format!(
"encrypt: expected CurrentUserAtom (0x{:04X}), got 0x{:04X}",
RT_CURRENT_USER_ATOM, cu_type
)));
}
let header_token = read_u32_le(cu_data, 12)?;
if header_token == HEADER_TOKEN_ENCRYPTED {
return Err(Error::ppt97(
"encrypt: file already encrypted (headerToken=0xF3D1C4DF)".to_string(),
));
}
let offset_to_current_edit = read_u32_le(cu_data, 16)?;
let ue_offset = offset_to_current_edit as usize;
let (_, _, ue_type, ue_len) = parse_record_header(ppt_data, ue_offset)?;
if ue_type != RT_USER_EDIT_ATOM {
return Err(Error::ppt97(format!(
"encrypt: expected UserEditAtom (0x{:04X}), got 0x{:04X}",
RT_USER_EDIT_ATOM, ue_type
)));
}
if ue_len != 28 {
return Err(Error::ppt97(format!(
"encrypt: file already encrypted or malformed (UserEditAtom.recLen={}, expected 28)",
ue_len
)));
}
let offset_persist_dir = read_u32_le(ppt_data, ue_offset + 20)?;
let persist_entries = parse_persist_directory(ppt_data, offset_persist_dir as usize)?;
let persist_entries =
reorder_persist_objects(ppt_data, &persist_entries, offset_persist_dir as usize)?;
let mut rng = rand::thread_rng();
let salt: Vec<u8> = (0..SALT_SIZE).map(|_| rng.gen()).collect();
let verifier_plain: Vec<u8> = (0..VERIFIER_SIZE).map(|_| rng.gen()).collect();
let verifier_hash = Sha1::digest(&verifier_plain).to_vec();
let key_block0 = make_key(password, &salt, KEY_SIZE_BITS, 0);
let mut rc4 = Rc4::new(&key_block0);
let mut encrypted_verifier = verifier_plain.clone();
rc4.process(&mut encrypted_verifier);
let mut encrypted_verifier_hash = verifier_hash.clone();
rc4.process(&mut encrypted_verifier_hash);
let mut sorted_entries = persist_entries.clone();
sorted_entries.sort_by_key(|(_, offset)| *offset);
for (pid, poff) in &sorted_entries {
let poff = *poff as usize;
let (_, _, rec_type, rec_len) = parse_record_header(ppt_data, poff)?;
if rec_type == RT_USER_EDIT_ATOM || rec_type == RT_PERSIST_DIRECTORY_ATOM {
continue;
}
let total_len = 8 + rec_len as usize;
let record_data = &mut ppt_data[poff..poff + total_len];
let encrypted = encrypt_persist_object(password, &salt, KEY_SIZE_BITS, record_data, *pid);
record_data.copy_from_slice(&encrypted);
}
let crypt_session = build_crypt_session10_container(
&salt,
&encrypted_verifier,
&encrypted_verifier_hash,
KEY_SIZE_BITS,
);
let crypt_session_offset = offset_persist_dir as u32;
let crypt_session_len = crypt_session.len();
ppt_data.splice(
offset_persist_dir as usize..offset_persist_dir as usize,
crypt_session.iter().copied(),
);
let pd_offset_new = offset_persist_dir as usize + crypt_session_len;
let ue_offset_after_cs = ue_offset + crypt_session_len;
let pd_data_start = pd_offset_new + 8;
let entry_val = read_u32_le(ppt_data, pd_data_start)?;
let entry_pid = entry_val & 0xFFFFF;
let entry_cpersist = (entry_val >> 20) & 0xFFF;
let crypt_session_pid = entry_pid + entry_cpersist;
let insert_pos = pd_data_start + 4 + entry_cpersist as usize * 4;
ppt_data.splice(insert_pos..insert_pos, std::iter::once(0u8).cycle().take(4));
let (_, _, _, pd_len) = parse_record_header(ppt_data, pd_offset_new)?;
write_u32_le(ppt_data, pd_offset_new + 4, pd_len + 4)?;
let new_entry_val = entry_pid | ((entry_cpersist + 1) << 20);
write_u32_le(ppt_data, pd_data_start, new_entry_val)?;
let ue_offset_new = ue_offset_after_cs + 4;
ppt_data.extend_from_slice(&[0u8; 4]);
write_u32_le(ppt_data, ue_offset_new + 4, 32)?;
write_u32_le(ppt_data, ue_offset_new + 8 + 28, crypt_session_pid)?;
write_u32_le(ppt_data, ue_offset_new + 28, crypt_session_pid)?;
write_u32_le(ppt_data, ue_offset_new + 20, pd_offset_new as u32)?;
write_u32_le(ppt_data, insert_pos, crypt_session_offset)?;
write_u32_le(cu_data, 12, HEADER_TOKEN_ENCRYPTED)?;
write_u32_le(cu_data, 16, ue_offset_new as u32)?;
Ok(())
}