use flate2::read::ZlibDecoder;
use md5::{Digest as _, Md5};
use sha1::{Digest as _, Sha1};
use std::io::Read as _;
const EVF_SIGNATURE: [u8; 8] = [0x45, 0x56, 0x46, 0x09, 0x0d, 0x0a, 0xff, 0x00];
const FILE_HEADER_SIZE: usize = 13;
pub(crate) const SECTION_DESCRIPTOR_SIZE: usize = 76;
const VOLUME_DATA_MIN: usize = 24;
const KNOWN_TYPES: &[&str] = &[
"header", "header2", "volume", "disk", "table", "table2", "sectors", "hash", "digest",
"error2", "session", "done", "next", "data", "ltree", "ltreedata",
];
const EVF2_SIGNATURE: [u8; 8] = [0x45, 0x56, 0x46, 0x32, 0x0d, 0x0a, 0x81, 0x00];
const LEF2_SIGNATURE: [u8; 8] = [0x4c, 0x45, 0x46, 0x32, 0x0d, 0x0a, 0x81, 0x00];
const EVF2_FILE_HEADER_SIZE: usize = 32;
const EVF2_SECTION_DESCRIPTOR_SIZE: usize = 64;
const EVF2_DATA_FLAG_ENCRYPTED: u32 = 0x0000_0002;
const EVF2_TYPE_MD5_HASH: u32 = 0x08;
const EVF2_TYPE_SHA1_HASH: u32 = 0x09;
const EVF2_TYPE_DONE: u32 = 0x0F;
const EVF2_TYPE_NEXT: u32 = 0x0D;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Severity {
Info,
Warning,
Error,
Critical,
}
#[derive(Debug, Clone)]
pub enum EwfIntegrityAnomaly {
InvalidSignature,
SegmentNumberZero,
SectionDescriptorCrcMismatch {
offset: u64,
section_type: String,
computed: u32,
stored: u32,
},
SectionChainBroken {
at_offset: u64,
next_offset: u64,
},
SectionGapNonZero {
gap_offset: u64,
gap_size: u64,
},
VolumeSectionMissing,
UnknownSectionType {
offset: u64,
type_name: String,
},
DoneSectionMissing,
ChunkSizeInvalid {
sectors_per_chunk: u32,
bytes_per_sector: u32,
},
SectorCountMismatch {
declared: u64,
expected: u64,
},
BytesPerSectorInvalid {
bytes_per_sector: u32,
},
TableChunkCountMismatch {
in_volume: u32,
in_table: u32,
},
TableEntryOutOfBounds {
chunk_index: u32,
entry_offset: u64,
file_size: u64,
},
TableEntryOutsideSectorsRange {
chunk_index: u32,
entry_offset: u64,
sectors_start: u64,
sectors_end: u64,
},
SectionGapZero {
gap_offset: u64,
gap_size: u64,
},
HashMismatch {
computed: [u8; 16],
stored: [u8; 16],
},
HashSectionMissing,
SegmentOutOfOrder {
segment_number: u16,
expected: u16,
},
DigestSha1Mismatch {
computed: [u8; 20],
stored: [u8; 20],
},
ExternalMd5Mismatch {
computed: [u8; 16],
expected: [u8; 16],
},
ExternalSha1Mismatch {
computed: [u8; 20],
expected: [u8; 20],
},
Ewf2SectionDataHashMismatch {
offset: u64,
section_type_id: u32,
computed: [u8; 16],
stored: [u8; 16],
},
Ewf2EncryptedSection {
offset: u64,
},
Ewf2HashSectionMissing,
}
impl EwfIntegrityAnomaly {
pub fn severity(&self) -> Severity {
match self {
Self::InvalidSignature => Severity::Critical,
Self::SegmentNumberZero => Severity::Error,
Self::SectionDescriptorCrcMismatch { .. } => Severity::Error,
Self::SectionChainBroken { .. } => Severity::Critical,
Self::SectionGapNonZero { .. } => Severity::Warning,
Self::VolumeSectionMissing => Severity::Critical,
Self::UnknownSectionType { .. } => Severity::Warning,
Self::DoneSectionMissing => Severity::Warning,
Self::ChunkSizeInvalid { .. } => Severity::Error,
Self::SectorCountMismatch { .. } => Severity::Error,
Self::BytesPerSectorInvalid { .. } => Severity::Error,
Self::TableChunkCountMismatch { .. } => Severity::Error,
Self::TableEntryOutOfBounds { .. } => Severity::Error,
Self::TableEntryOutsideSectorsRange { .. } => Severity::Error,
Self::SectionGapZero { .. } => Severity::Info,
Self::HashMismatch { .. } => Severity::Error,
Self::HashSectionMissing => Severity::Warning,
Self::SegmentOutOfOrder { .. } => Severity::Error,
Self::DigestSha1Mismatch { .. } => Severity::Error,
Self::ExternalMd5Mismatch { .. } => Severity::Critical,
Self::ExternalSha1Mismatch { .. } => Severity::Critical,
Self::Ewf2SectionDataHashMismatch { .. } => Severity::Error,
Self::Ewf2EncryptedSection { .. } => Severity::Warning,
Self::Ewf2HashSectionMissing => Severity::Warning,
}
}
}
pub struct EwfIntegrity<'a> {
segments: Vec<&'a [u8]>,
expected_md5: Option<[u8; 16]>,
expected_sha1: Option<[u8; 20]>,
}
impl<'a> EwfIntegrity<'a> {
pub fn new(data: &'a [u8]) -> Self {
Self {
segments: vec![data],
expected_md5: None,
expected_sha1: None,
}
}
pub fn from_segments(segs: &[&'a [u8]]) -> Self {
Self {
segments: segs.to_vec(),
expected_md5: None,
expected_sha1: None,
}
}
pub fn with_expected_md5(mut self, hash: [u8; 16]) -> Self {
self.expected_md5 = Some(hash);
self
}
pub fn with_expected_sha1(mut self, hash: [u8; 20]) -> Self {
self.expected_sha1 = Some(hash);
self
}
pub fn analyse(&self) -> Vec<EwfIntegrityAnomaly> {
let first = self.segments.first().copied().unwrap_or(&[]);
if first.len() >= 8
&& (first[0..8] == EVF2_SIGNATURE || first[0..8] == LEF2_SIGNATURE)
{
return self.analyse_all_ewf2();
}
self.analyse_all_ewf1()
}
fn analyse_all_ewf1(&self) -> Vec<EwfIntegrityAnomaly> {
let mut issues = Vec::new();
let n = self.segments.len();
let multi = n > 1;
let mut geometry: Option<VolumeGeometry> = None;
let mut all_sections: Vec<Vec<Section>> = Vec::with_capacity(n);
for (idx, &data) in self.segments.iter().enumerate() {
let expected_seg_num = (idx + 1) as u16;
let is_last = idx == n - 1;
let file_size = data.len() as u64;
if data.len() < FILE_HEADER_SIZE {
issues.push(EwfIntegrityAnomaly::SectionChainBroken {
at_offset: 0,
next_offset: 0,
});
all_sections.push(Vec::new());
continue;
}
if data[0..8] != EVF_SIGNATURE {
issues.push(EwfIntegrityAnomaly::InvalidSignature);
}
let seg_num = u16::from_le_bytes(data[9..11].try_into().unwrap());
if seg_num == 0 {
issues.push(EwfIntegrityAnomaly::SegmentNumberZero);
} else if seg_num != expected_seg_num {
issues.push(EwfIntegrityAnomaly::SegmentOutOfOrder {
segment_number: seg_num,
expected: expected_seg_num,
});
}
let sections = walk_sections_v1(data, &mut issues);
if idx == 0 {
match sections
.iter()
.find(|s| s.type_name == "volume" || s.type_name == "disk")
{
None => issues.push(EwfIntegrityAnomaly::VolumeSectionMissing),
Some(v) => geometry = check_volume_v1(data, v.offset, &mut issues),
}
}
let vol_count = if !multi && idx == 0 {
geometry.as_ref().map(|g| g.chunk_count)
} else {
None
};
let sectors_range = sections
.iter()
.find(|s| s.type_name == "sectors")
.map(|s| (s.offset + SECTION_DESCRIPTOR_SIZE as u64, s.offset + s.size));
if let Some(table) = sections.iter().find(|s| s.type_name == "table") {
check_table_v1(
data,
table.offset,
vol_count,
file_size,
sectors_range,
&mut issues,
);
}
if is_last && !sections.iter().any(|s| s.type_name == "done") {
issues.push(EwfIntegrityAnomaly::DoneSectionMissing);
}
all_sections.push(sections);
}
if let Some(geom) = &geometry {
check_hash_all_segments(
&self.segments,
&all_sections,
geom,
self.expected_md5,
self.expected_sha1,
&mut issues,
);
}
issues
}
fn analyse_all_ewf2(&self) -> Vec<EwfIntegrityAnomaly> {
let mut issues = Vec::new();
let n = self.segments.len();
for (idx, &data) in self.segments.iter().enumerate() {
let expected_seg_num = (idx + 1) as u32;
if data.len() < EVF2_FILE_HEADER_SIZE {
issues.push(EwfIntegrityAnomaly::SectionChainBroken {
at_offset: 0,
next_offset: 0,
});
continue;
}
if data[0..8] != EVF2_SIGNATURE && data[0..8] != LEF2_SIGNATURE {
issues.push(EwfIntegrityAnomaly::InvalidSignature);
}
let seg_num = u32::from_le_bytes(data[12..16].try_into().unwrap());
if seg_num == 0 {
issues.push(EwfIntegrityAnomaly::SegmentNumberZero);
} else if seg_num != expected_seg_num {
issues.push(EwfIntegrityAnomaly::SegmentOutOfOrder {
segment_number: seg_num as u16,
expected: expected_seg_num as u16,
});
}
let mut pos = EVF2_FILE_HEADER_SIZE;
let mut has_hash = false;
loop {
if pos + EVF2_SECTION_DESCRIPTOR_SIZE > data.len() {
break;
}
let desc = &data[pos..pos + EVF2_SECTION_DESCRIPTOR_SIZE];
let section_type = u32::from_le_bytes(desc[0..4].try_into().unwrap());
let data_flags = u32::from_le_bytes(desc[4..8].try_into().unwrap());
let data_size = u64::from_le_bytes(desc[16..24].try_into().unwrap()) as usize;
let padding_size = u32::from_le_bytes(desc[28..32].try_into().unwrap()) as usize;
let stored_hash: [u8; 16] = desc[32..48].try_into().unwrap();
let body_start = pos + EVF2_SECTION_DESCRIPTOR_SIZE;
let body_end = body_start.saturating_add(data_size);
if data_flags & EVF2_DATA_FLAG_ENCRYPTED != 0 {
issues.push(EwfIntegrityAnomaly::Ewf2EncryptedSection {
offset: pos as u64,
});
} else if stored_hash != [0u8; 16] {
if let Some(body) = data.get(body_start..body_end) {
let computed: [u8; 16] = Md5::digest(body).into();
if computed != stored_hash {
issues.push(EwfIntegrityAnomaly::Ewf2SectionDataHashMismatch {
offset: pos as u64,
section_type_id: section_type,
computed,
stored: stored_hash,
});
}
}
}
if section_type == EVF2_TYPE_MD5_HASH || section_type == EVF2_TYPE_SHA1_HASH {
has_hash = true;
}
if section_type == EVF2_TYPE_DONE || section_type == EVF2_TYPE_NEXT {
break;
}
let next_pos = body_end.saturating_add(padding_size);
if next_pos <= pos {
issues.push(EwfIntegrityAnomaly::SectionChainBroken {
at_offset: pos as u64,
next_offset: next_pos as u64,
});
break;
}
pos = next_pos;
}
if idx == n - 1 && !has_hash {
issues.push(EwfIntegrityAnomaly::Ewf2HashSectionMissing);
}
}
issues
}
}
struct Section {
type_name: String,
offset: u64,
size: u64,
}
struct VolumeGeometry {
chunk_count: u32,
sectors_per_chunk: u32,
bytes_per_sector: u32,
sector_count: u64,
}
fn walk_sections_v1(data: &[u8], issues: &mut Vec<EwfIntegrityAnomaly>) -> Vec<Section> {
let file_size = data.len() as u64;
let mut sections = Vec::new();
let mut pos = FILE_HEADER_SIZE as u64;
loop {
let off = pos as usize;
if off + SECTION_DESCRIPTOR_SIZE > data.len() {
break;
}
let desc = &data[off..off + SECTION_DESCRIPTOR_SIZE];
let type_end = desc[..16].iter().position(|&b| b == 0).unwrap_or(16);
let type_name = String::from_utf8_lossy(&desc[..type_end]).into_owned();
let stored_crc = u32::from_le_bytes(desc[72..76].try_into().unwrap());
let computed_crc = adler32(&desc[..72]);
if computed_crc != stored_crc {
issues.push(EwfIntegrityAnomaly::SectionDescriptorCrcMismatch {
offset: pos,
section_type: type_name.clone(),
computed: computed_crc,
stored: stored_crc,
});
}
if !KNOWN_TYPES.contains(&type_name.as_str()) {
issues.push(EwfIntegrityAnomaly::UnknownSectionType {
offset: pos,
type_name: type_name.clone(),
});
}
let next = u64::from_le_bytes(desc[16..24].try_into().unwrap());
let section_size = u64::from_le_bytes(desc[24..32].try_into().unwrap());
let section_end = pos.saturating_add(section_size);
sections.push(Section {
type_name: type_name.clone(),
offset: pos,
size: section_size,
});
if type_name == "done" || type_name == "next" {
break;
}
if next == 0 || next > file_size || next <= pos {
issues.push(EwfIntegrityAnomaly::SectionChainBroken {
at_offset: pos,
next_offset: next,
});
break;
}
if next > section_end {
let gap_offset = section_end;
let gap_size = next - section_end;
let non_zero = data
.get(section_end as usize..next as usize)
.map(|s| s.iter().any(|&b| b != 0))
.unwrap_or(false);
if non_zero {
issues.push(EwfIntegrityAnomaly::SectionGapNonZero { gap_offset, gap_size });
} else {
issues.push(EwfIntegrityAnomaly::SectionGapZero { gap_offset, gap_size });
}
}
pos = next;
}
sections
}
fn check_volume_v1(
data: &[u8],
desc_offset: u64,
issues: &mut Vec<EwfIntegrityAnomaly>,
) -> Option<VolumeGeometry> {
let data_start = (desc_offset as usize) + SECTION_DESCRIPTOR_SIZE;
if data.len() < data_start + VOLUME_DATA_MIN {
return None;
}
let vol = &data[data_start..];
let chunk_count = u32::from_le_bytes(vol[4..8].try_into().unwrap());
let sectors_per_chunk = u32::from_le_bytes(vol[8..12].try_into().unwrap());
let bytes_per_sector = u32::from_le_bytes(vol[12..16].try_into().unwrap());
let sector_count = u64::from_le_bytes(vol[16..24].try_into().unwrap());
if bytes_per_sector != 512 && bytes_per_sector != 4096 {
issues.push(EwfIntegrityAnomaly::BytesPerSectorInvalid { bytes_per_sector });
}
if sectors_per_chunk == 0 || !sectors_per_chunk.is_power_of_two() {
issues.push(EwfIntegrityAnomaly::ChunkSizeInvalid {
sectors_per_chunk,
bytes_per_sector,
});
}
let max_sectors = u64::from(chunk_count) * u64::from(sectors_per_chunk);
let min_sectors = max_sectors.saturating_sub(u64::from(sectors_per_chunk));
if sectors_per_chunk.is_power_of_two() {
let out_of_range =
sector_count > max_sectors || (chunk_count > 0 && sector_count <= min_sectors);
if out_of_range {
issues.push(EwfIntegrityAnomaly::SectorCountMismatch {
declared: sector_count,
expected: max_sectors,
});
}
}
Some(VolumeGeometry {
chunk_count,
sectors_per_chunk,
bytes_per_sector,
sector_count,
})
}
fn check_table_v1(
data: &[u8],
desc_offset: u64,
volume_chunk_count: Option<u32>,
file_size: u64,
sectors_range: Option<(u64, u64)>,
issues: &mut Vec<EwfIntegrityAnomaly>,
) {
let data_start = (desc_offset as usize) + SECTION_DESCRIPTOR_SIZE;
if data.len() < data_start + 24 {
return;
}
let tbl = &data[data_start..];
let entry_count = u32::from_le_bytes(tbl[0..4].try_into().unwrap());
let base_offset = u64::from_le_bytes(tbl[8..16].try_into().unwrap());
if let Some(vol_count) = volume_chunk_count {
if entry_count != vol_count {
issues.push(EwfIntegrityAnomaly::TableChunkCountMismatch {
in_volume: vol_count,
in_table: entry_count,
});
}
}
let entries_start = data_start + 24;
for i in 0..entry_count {
let entry_off = entries_start + (i as usize) * 4;
if entry_off + 4 > data.len() {
break;
}
let raw = u32::from_le_bytes(data[entry_off..entry_off + 4].try_into().unwrap());
let chunk_rel = u64::from(raw & 0x7FFF_FFFF);
let absolute = base_offset.saturating_add(chunk_rel);
if absolute >= file_size {
issues.push(EwfIntegrityAnomaly::TableEntryOutOfBounds {
chunk_index: i,
entry_offset: absolute,
file_size,
});
} else if let Some((sec_start, sec_end)) = sectors_range {
if absolute < sec_start || absolute >= sec_end {
issues.push(EwfIntegrityAnomaly::TableEntryOutsideSectorsRange {
chunk_index: i,
entry_offset: absolute,
sectors_start: sec_start,
sectors_end: sec_end,
});
}
}
}
}
fn iter_segment_chunks(data: &[u8], sections: &[Section]) -> Vec<(usize, usize, bool)> {
let table = match sections.iter().find(|s| s.type_name == "table") {
Some(s) => s,
None => return Vec::new(),
};
let sectors = match sections.iter().find(|s| s.type_name == "sectors") {
Some(s) => s,
None => return Vec::new(),
};
let tbl_data_start = (table.offset as usize) + SECTION_DESCRIPTOR_SIZE;
if data.len() < tbl_data_start + 24 {
return Vec::new();
}
let tbl = &data[tbl_data_start..];
let entry_count = u32::from_le_bytes(tbl[0..4].try_into().unwrap()) as usize;
let base_offset = u64::from_le_bytes(tbl[8..16].try_into().unwrap()) as usize;
let entries_start = tbl_data_start + 24;
let sectors_body_end = (sectors.offset + sectors.size) as usize;
let mut chunks = Vec::with_capacity(entry_count);
for i in 0..entry_count {
let entry_off = entries_start + i * 4;
if entry_off + 4 > data.len() {
break;
}
let raw = u32::from_le_bytes(data[entry_off..entry_off + 4].try_into().unwrap());
let compressed = raw & 0x8000_0000 != 0;
let rel = (raw & 0x7FFF_FFFF) as usize;
let start = base_offset + rel;
let end = if i + 1 < entry_count {
let next_off = entries_start + (i + 1) * 4;
if next_off + 4 > data.len() {
break;
}
let next_raw = u32::from_le_bytes(data[next_off..next_off + 4].try_into().unwrap());
let next_rel = (next_raw & 0x7FFF_FFFF) as usize;
base_offset + next_rel
} else {
sectors_body_end.min(data.len())
};
if start >= end || end > data.len() {
break;
}
chunks.push((start, end, compressed));
}
chunks
}
fn check_hash_all_segments(
segments: &[&[u8]],
all_sections: &[Vec<Section>],
geom: &VolumeGeometry,
expected_md5: Option<[u8; 16]>,
expected_sha1: Option<[u8; 20]>,
issues: &mut Vec<EwfIntegrityAnomaly>,
) {
let chunk_size = u64::from(geom.sectors_per_chunk) * u64::from(geom.bytes_per_sector);
let total_bytes = geom.sector_count * u64::from(geom.bytes_per_sector);
let mut bytes_remaining = total_bytes;
let mut md5_h = Md5::new();
let mut sha1_h = Sha1::new();
'outer: for (&seg_data, sections) in segments.iter().zip(all_sections.iter()) {
for (start, end, compressed) in iter_segment_chunks(seg_data, sections) {
if bytes_remaining == 0 {
break 'outer;
}
let to_hash = bytes_remaining.min(chunk_size) as usize;
let raw = &seg_data[start..end];
if compressed {
let limit = (to_hash as u64).saturating_add(1);
let mut decompressed = Vec::with_capacity(to_hash);
if ZlibDecoder::new(raw)
.take(limit)
.read_to_end(&mut decompressed)
.is_err()
{
bytes_remaining = bytes_remaining.saturating_sub(to_hash as u64);
continue;
}
let slice = &decompressed[..decompressed.len().min(to_hash)];
md5_h.update(slice);
sha1_h.update(slice);
} else {
let slice = &raw[..raw.len().min(to_hash)];
md5_h.update(slice);
sha1_h.update(slice);
}
bytes_remaining = bytes_remaining.saturating_sub(to_hash as u64);
}
}
let computed_md5: [u8; 16] = md5_h.finalize().into();
let computed_sha1: [u8; 20] = sha1_h.finalize().into();
let last_sections = match all_sections.last() {
Some(s) => s,
None => return,
};
let last_data = match segments.last() {
Some(d) => d,
None => return,
};
match last_sections.iter().find(|s| s.type_name == "hash") {
Some(hash_sec) => {
let body_start = (hash_sec.offset as usize) + SECTION_DESCRIPTOR_SIZE;
if let Some(stored_slice) = last_data.get(body_start..body_start + 16) {
let stored: [u8; 16] = stored_slice.try_into().unwrap();
if computed_md5 != stored {
issues.push(EwfIntegrityAnomaly::HashMismatch {
computed: computed_md5,
stored,
});
}
}
}
None => issues.push(EwfIntegrityAnomaly::HashSectionMissing),
}
if let Some(digest_sec) = last_sections.iter().find(|s| s.type_name == "digest") {
let body_start = (digest_sec.offset as usize) + SECTION_DESCRIPTOR_SIZE;
if let Some(sha1_slice) = last_data.get(body_start + 16..body_start + 36) {
let stored: [u8; 20] = sha1_slice.try_into().unwrap();
if stored != [0u8; 20] && computed_sha1 != stored {
issues.push(EwfIntegrityAnomaly::DigestSha1Mismatch {
computed: computed_sha1,
stored,
});
}
}
}
if let Some(expected) = expected_md5 {
if computed_md5 != expected {
issues.push(EwfIntegrityAnomaly::ExternalMd5Mismatch {
computed: computed_md5,
expected,
});
}
}
if let Some(expected) = expected_sha1 {
if computed_sha1 != expected {
issues.push(EwfIntegrityAnomaly::ExternalSha1Mismatch {
computed: computed_sha1,
expected,
});
}
}
}
pub(crate) fn adler32(data: &[u8]) -> u32 {
const MOD: u32 = 65521;
let mut s1: u32 = 1;
let mut s2: u32 = 0;
for &b in data {
s1 = (s1 + u32::from(b)) % MOD;
s2 = (s2 + s1) % MOD;
}
(s2 << 16) | s1
}