use anyhow::{Context, Result};
use cfb::CompoundFile;
use sha2::{Digest, Sha256};
use std::io::{Cursor, Read};
use std::path::PathBuf;
const OLE_MAGIC: [u8; 8] = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
const DIGITAL_SIGNATURE: &str = "\u{5}DigitalSignature";
const DIGITAL_SIGNATURE_EX: &str = "\u{5}MsiDigitalSignatureEx";
pub fn is_compound_file(bytes: &[u8]) -> bool {
bytes.starts_with(&OLE_MAGIC)
}
fn cmp_hash(a: &str, b: &str) -> std::cmp::Ordering {
let na: Vec<u8> = a.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
let nb: Vec<u8> = b.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
let m = na.len().min(nb.len());
match na[..m].cmp(&nb[..m]) {
std::cmp::Ordering::Equal => nb.len().cmp(&na.len()), other => other,
}
}
fn clsid_cfb_bytes(u: &uuid::Uuid) -> [u8; 16] {
let b = u.as_bytes();
[
b[3], b[2], b[1], b[0], b[5], b[4], b[7], b[6], b[8], b[9], b[10], b[11], b[12], b[13],
b[14], b[15],
]
}
pub fn msi_hash(bytes: &[u8]) -> Result<[u8; 32]> {
let mut cf = CompoundFile::open(Cursor::new(bytes)).context("opening compound file")?;
let mut h = Sha256::new();
hash_storage(&mut cf, PathBuf::from("/"), true, &mut h)?;
Ok(h.finalize().into())
}
fn hash_storage(
cf: &mut CompoundFile<Cursor<&[u8]>>,
path: PathBuf,
is_root: bool,
h: &mut Sha256,
) -> Result<()> {
let mut children: Vec<(String, PathBuf, bool)> = cf
.read_storage(&path)
.with_context(|| format!("reading storage {}", path.display()))?
.map(|e| (e.name().to_string(), e.path().to_path_buf(), e.is_stream()))
.collect();
children.sort_by(|a, b| cmp_hash(&a.0, &b.0));
let clsid = clsid_cfb_bytes(cf.entry(&path).context("storage entry")?.clsid());
for (name, child_path, is_stream) in children {
if is_root && (name == DIGITAL_SIGNATURE || name == DIGITAL_SIGNATURE_EX) {
continue;
}
if is_stream {
let mut data = Vec::new();
cf.open_stream(&child_path)
.with_context(|| format!("opening stream {}", child_path.display()))?
.read_to_end(&mut data)
.context("reading stream")?;
if data.is_empty() {
continue;
}
h.update(&data);
} else {
hash_storage(cf, child_path, false, h)?;
}
}
h.update(clsid);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore = "MSI digest does not yet match osslsigncode — see comment"]
fn msi_hash_matches_osslsigncode() {
let msi = include_bytes!("../tests/fixtures/test.msi");
assert!(is_compound_file(msi));
assert_eq!(
hex::encode_upper(msi_hash(msi).unwrap()),
"CB45D9776CEE01B97E0EB14D007FB2779EBE25C0DCAB25F4A1CAE947C3B19A61"
);
}
}