#![cfg_attr(not(test), deny(clippy::arithmetic_side_effects))]
use flate2::read::ZlibDecoder;
use md5::{Digest as _, Md5};
use sha1::Sha1;
use sha2::Sha256;
use std::fmt;
use std::io::Read as _;
use ewf::sections::{
self, EwfVolume, SectionDescriptor, TableHeader, EVF_SIGNATURE, FILE_HEADER_SIZE,
SECTION_DESCRIPTOR_SIZE as EWF_SECTION_DESCRIPTOR_SIZE, TABLE_HEADER_SIZE,
};
pub(crate) const SECTION_DESCRIPTOR_SIZE: usize = EWF_SECTION_DESCRIPTOR_SIZE;
const DVF_SIGNATURE: [u8; 8] = [0x64, 0x76, 0x66, 0x09, 0x0d, 0x0a, 0xff, 0x00];
const LVF_SIGNATURE: [u8; 8] = [0x4c, 0x56, 0x46, 0x09, 0x0d, 0x0a, 0xff, 0x00];
const VOLUME_DATA_MIN: usize = 24;
const VALID_MEDIA_TYPES: &[u8] = &[0x00, 0x01, 0x03, 0x0e, 0x10];
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_CHUNK_FLAG_COMPRESSED: u32 = 0x0000_0001;
const EVF2_TYPE_MEDIA_INFO: u32 = 0x02;
const EVF2_TYPE_CHUNK_TABLE: u32 = 0x04;
const EVF2_TYPE_MD5_HASH: u32 = 0x08;
const EVF2_TYPE_SHA1_HASH: u32 = 0x09;
const EVF2_TYPE_SHA256_HASH: u32 = 0x0A;
const EVF2_CHUNK_TABLE_HEADER_SIZE: usize = 32;
const EVF2_CHUNK_TABLE_ENTRY_SIZE: usize = 16;
pub use forensicnomicon::report::Severity;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
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,
SectorsSectionMissing,
TableSectionMissing,
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,
},
TableHeaderAdler32Mismatch {
computed: u32,
stored: 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,
Table2Mismatch {
offset: usize,
},
BadSectorsPresent {
count: u32,
},
SegmentOutOfOrder {
segment_number: u16,
expected: u16,
},
DigestSha1Mismatch {
computed: [u8; 20],
stored: [u8; 20],
},
DigestSha256Mismatch {
computed: [u8; 32],
stored: [u8; 32],
},
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,
VolumeBodyCrcMismatch {
computed: u32,
stored: u32,
},
MediaTypeUnknown {
media_type: u8,
},
SetIdentifierMismatch {
segment: usize,
},
Ewf2MediaInfoMissing,
Ewf2ChunkTableChecksumMismatch {
computed: u32,
stored: u32,
},
ChunkChecksumMismatch {
chunk_index: usize,
computed: u32,
stored: u32,
},
ChunkDecompressionError {
chunk_index: usize,
},
UnsupportedCompressionAlgorithm {
method_id: u16,
},
ExternalSha256Mismatch {
computed: [u8; 32],
expected: [u8; 32],
},
Ewf2MediaInfoParseFailed,
}
impl EwfIntegrityAnomaly {
pub fn severity(&self) -> Severity {
match self {
Self::InvalidSignature => Severity::Critical,
Self::SegmentNumberZero => Severity::High,
Self::SectionDescriptorCrcMismatch { .. } => Severity::High,
Self::SectionChainBroken { .. } => Severity::Critical,
Self::SectionGapNonZero { .. } => Severity::Medium,
Self::VolumeSectionMissing => Severity::Critical,
Self::UnknownSectionType { .. } => Severity::Medium,
Self::DoneSectionMissing => Severity::Medium,
Self::SectorsSectionMissing => Severity::High,
Self::TableSectionMissing => Severity::High,
Self::ChunkSizeInvalid { .. } => Severity::High,
Self::SectorCountMismatch { .. } => Severity::High,
Self::BytesPerSectorInvalid { .. } => Severity::High,
Self::TableChunkCountMismatch { .. } => Severity::High,
Self::TableHeaderAdler32Mismatch { .. } => Severity::High,
Self::TableEntryOutOfBounds { .. } => Severity::High,
Self::TableEntryOutsideSectorsRange { .. } => Severity::High,
Self::SectionGapZero { .. } => Severity::Info,
Self::HashMismatch { .. } => Severity::High,
Self::HashSectionMissing => Severity::Medium,
Self::Table2Mismatch { .. } => Severity::High,
Self::BadSectorsPresent { .. } => Severity::Medium,
Self::SegmentOutOfOrder { .. } => Severity::High,
Self::DigestSha1Mismatch { .. } => Severity::High,
Self::DigestSha256Mismatch { .. } => Severity::High,
Self::ExternalMd5Mismatch { .. } => Severity::Critical,
Self::ExternalSha1Mismatch { .. } => Severity::Critical,
Self::VolumeBodyCrcMismatch { .. } => Severity::High,
Self::MediaTypeUnknown { .. } => Severity::Medium,
Self::SetIdentifierMismatch { .. } => Severity::High,
Self::Ewf2SectionDataHashMismatch { .. } => Severity::High,
Self::Ewf2EncryptedSection { .. } => Severity::Medium,
Self::Ewf2HashSectionMissing => Severity::Medium,
Self::Ewf2MediaInfoMissing => Severity::Medium,
Self::Ewf2ChunkTableChecksumMismatch { .. } => Severity::High,
Self::ChunkChecksumMismatch { .. } => Severity::High,
Self::ChunkDecompressionError { .. } => Severity::High,
Self::UnsupportedCompressionAlgorithm { .. } => Severity::High,
Self::ExternalSha256Mismatch { .. } => Severity::Critical,
Self::Ewf2MediaInfoParseFailed => Severity::High,
}
}
}
impl fmt::Display for EwfIntegrityAnomaly {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidSignature =>
write!(f, "invalid EWF signature — not a valid E01/Ex01 file"),
Self::SegmentNumberZero =>
write!(f, "segment number is zero (expected ≥ 1)"),
Self::SectionDescriptorCrcMismatch { offset, section_type, computed, stored } =>
write!(f, "section '{section_type}' at 0x{offset:x}: descriptor CRC mismatch (computed 0x{computed:08x}, stored 0x{stored:08x})"),
Self::SectionChainBroken { at_offset, next_offset } =>
write!(f, "section chain broken at 0x{at_offset:x}: next pointer 0x{next_offset:x} is invalid"),
Self::SectionGapNonZero { gap_offset, gap_size } =>
write!(f, "non-zero data in {gap_size}-byte gap at 0x{gap_offset:x} — possible hidden data"),
Self::VolumeSectionMissing =>
write!(f, "volume/disk section missing in segment 1"),
Self::UnknownSectionType { offset, type_name } =>
write!(f, "unknown section type '{type_name}' at 0x{offset:x}"),
Self::DoneSectionMissing =>
write!(f, "done section missing from final segment"),
Self::SectorsSectionMissing =>
write!(f, "sectors section missing — chunk data not found in segment"),
Self::TableSectionMissing =>
write!(f, "table section missing — chunk offset table not found in segment"),
Self::ChunkSizeInvalid { sectors_per_chunk, bytes_per_sector } =>
write!(f, "invalid chunk size: {sectors_per_chunk} sectors × {bytes_per_sector} bytes/sector"),
Self::SectorCountMismatch { declared, expected } =>
write!(f, "sector count mismatch: declared {declared}, expected {expected}"),
Self::BytesPerSectorInvalid { bytes_per_sector } =>
write!(f, "invalid bytes_per_sector: {bytes_per_sector} (expected 512 or 4096)"),
Self::TableChunkCountMismatch { in_volume, in_table } =>
write!(f, "chunk count mismatch: volume declares {in_volume}, table has {in_table}"),
Self::TableHeaderAdler32Mismatch { computed, stored } =>
write!(f, "table header Adler-32 mismatch: computed 0x{computed:08x}, stored 0x{stored:08x}"),
Self::TableEntryOutOfBounds { chunk_index, entry_offset, file_size } =>
write!(f, "table entry for chunk {chunk_index} points outside file: 0x{entry_offset:x} ≥ 0x{file_size:x}"),
Self::TableEntryOutsideSectorsRange { chunk_index, entry_offset, sectors_start, sectors_end } =>
write!(f, "table entry for chunk {chunk_index} at 0x{entry_offset:x} is outside sectors section [0x{sectors_start:x}..0x{sectors_end:x}]"),
Self::SectionGapZero { gap_offset, gap_size } =>
write!(f, "zero-padded {gap_size}-byte gap at 0x{gap_offset:x}"),
Self::HashMismatch { computed, stored } =>
write!(f, "MD5 mismatch: computed {}, stored {}", hex(computed), hex(stored)),
Self::HashSectionMissing =>
write!(f, "hash section missing — cannot verify MD5"),
Self::Table2Mismatch { offset } =>
write!(f, "table2 body differs from table at byte offset {offset} — one redundant copy is corrupt"),
Self::BadSectorsPresent { count } =>
write!(f, "error2 section reports {count} unreadable sector range(s) from acquisition"),
Self::SegmentOutOfOrder { segment_number, expected } =>
write!(f, "segment {segment_number} found where segment {expected} was expected"),
Self::DigestSha1Mismatch { computed, stored } =>
write!(f, "SHA-1 mismatch: computed {}, stored {}", hex(computed), hex(stored)),
Self::DigestSha256Mismatch { computed, stored } =>
write!(f, "SHA-256 mismatch: computed {}, stored {}", hex(computed), hex(stored)),
Self::ExternalMd5Mismatch { computed, expected } =>
write!(f, "MD5 does not match chain-of-custody reference: computed {}, expected {}", hex(computed), hex(expected)),
Self::ExternalSha1Mismatch { computed, expected } =>
write!(f, "SHA-1 does not match chain-of-custody reference: computed {}, expected {}", hex(computed), hex(expected)),
Self::ExternalSha256Mismatch { computed, expected } =>
write!(f, "SHA-256 does not match chain-of-custody reference: computed {}, expected {}", hex(computed), hex(expected)),
Self::Ewf2SectionDataHashMismatch { offset, section_type_id, computed, stored } =>
write!(f, "EWF v2 section (type 0x{section_type_id:02x}) at 0x{offset:x}: data integrity hash mismatch (computed {}, stored {})", hex(computed), hex(stored)),
Self::Ewf2EncryptedSection { offset } =>
write!(f, "EWF v2 encrypted section at 0x{offset:x} — content not verifiable"),
Self::Ewf2HashSectionMissing =>
write!(f, "EWF v2 hash section missing from final segment"),
Self::VolumeBodyCrcMismatch { computed, stored } =>
write!(f, "volume section body CRC mismatch (computed 0x{computed:08x}, stored 0x{stored:08x})"),
Self::MediaTypeUnknown { media_type } =>
write!(f, "unknown media_type 0x{media_type:02x}"),
Self::SetIdentifierMismatch { segment } =>
write!(f, "set_identifier GUID mismatch in segment {segment} — segments may be from different acquisitions"),
Self::Ewf2MediaInfoMissing =>
write!(f, "EWF v2 media information section missing"),
Self::Ewf2ChunkTableChecksumMismatch { computed, stored } =>
write!(f, "EWF v2 chunk table checksum mismatch (computed 0x{computed:08x}, stored 0x{stored:08x})"),
Self::ChunkChecksumMismatch { chunk_index, computed, stored } =>
write!(f, "chunk {chunk_index}: Adler-32 mismatch (computed 0x{computed:08x}, stored 0x{stored:08x})"),
Self::ChunkDecompressionError { chunk_index } =>
write!(f, "chunk {chunk_index}: zlib decompression failed — chunk data is corrupt"),
Self::UnsupportedCompressionAlgorithm { method_id } =>
write!(f, "EWF v2 file header specifies unsupported compression algorithm 0x{method_id:04x} — only deflate (0/1) is supported"),
Self::Ewf2MediaInfoParseFailed =>
write!(f, "EWF v2 media information section body could not be decompressed or decoded"),
}
}
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn le_u16(data: &[u8], off: usize) -> u16 {
let mut b = [0u8; 2];
if let Some(s) = data.get(off..off.saturating_add(2)) {
b.copy_from_slice(s);
}
u16::from_le_bytes(b)
}
fn le_u32(data: &[u8], off: usize) -> u32 {
let mut b = [0u8; 4];
if let Some(s) = data.get(off..off.saturating_add(4)) {
b.copy_from_slice(s);
}
u32::from_le_bytes(b)
}
fn le_u64(data: &[u8], off: usize) -> u64 {
let mut b = [0u8; 8];
if let Some(s) = data.get(off..off.saturating_add(8)) {
b.copy_from_slice(s);
}
u64::from_le_bytes(b)
}
fn array_at<const N: usize>(data: &[u8], off: usize) -> [u8; N] {
let mut b = [0u8; N];
if let Some(s) = data.get(off..off.saturating_add(N)) {
b.copy_from_slice(s);
}
b
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AnalysisProgress {
pub chunks_done: usize,
pub chunks_total: Option<usize>,
pub bytes_done: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ComputedHashes {
pub md5: [u8; 16],
pub sha1: [u8; 20],
pub sha256: [u8; 32],
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EwfHeaderMetadata {
pub description: String,
pub case_number: String,
pub evidence_number: String,
pub examiner_name: String,
pub acquisition_date: String,
pub system_date: String,
pub password_hash: String,
pub acquisition_software: String,
}
pub struct EwfIntegrity<'a> {
segments: Vec<&'a [u8]>,
expected_md5: Option<[u8; 16]>,
expected_sha1: Option<[u8; 20]>,
expected_sha256: Option<[u8; 32]>,
}
impl<'a> EwfIntegrity<'a> {
pub fn new(data: &'a [u8]) -> Self {
Self {
segments: vec![data],
expected_md5: None,
expected_sha1: None,
expected_sha256: None,
}
}
pub fn from_segments(segs: &[&'a [u8]]) -> Self {
Self {
segments: segs.to_vec(),
expected_md5: None,
expected_sha1: None,
expected_sha256: 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 with_expected_sha256(mut self, hash: [u8; 32]) -> Self {
self.expected_sha256 = Some(hash);
self
}
pub fn header_metadata(&self) -> Option<EwfHeaderMetadata> {
for &data in &self.segments {
if let Some(meta) = parse_header_section(data) {
return Some(meta);
}
}
None
}
pub fn compute_hashes(&self) -> Option<ComputedHashes> {
let first = self.segments.first().copied().unwrap_or(&[]);
if first.len() >= 8 && (first[0..8] == EVF2_SIGNATURE || first[0..8] == LEF2_SIGNATURE) {
return compute_hashes_ewf2(&self.segments);
}
compute_hashes_ewf1(&self.segments)
}
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()
}
pub fn analyse_with_progress(
&self,
progress: impl FnMut(AnalysisProgress),
) -> 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_with_progress(progress);
}
self.analyse_all_ewf1_with_progress(progress)
}
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);
let mut total_table_entries: u32 = 0;
for (idx, &data) in self.segments.iter().enumerate() {
let expected_seg_num = idx.saturating_add(1) as u16;
let is_last = idx == n.saturating_sub(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
&& data[0..8] != DVF_SIGNATURE
&& data[0..8] != LVF_SIGNATURE
{
issues.push(EwfIntegrityAnomaly::InvalidSignature);
}
let seg_num = le_u16(data, 9);
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 let Some(vol_sec) = sections
.iter()
.find(|s| s.type_name == "volume" || s.type_name == "disk")
{
if idx == 0 {
geometry = check_volume_v1(data, vol_sec.offset, vol_sec.size, &mut issues);
} else {
let later = check_volume_v1(data, vol_sec.offset, vol_sec.size, &mut issues);
if let (Some(ref base), Some(ref later_geom)) = (&geometry, &later) {
let base_guid = base.set_identifier;
let later_guid = later_geom.set_identifier;
let neither_zero = base_guid != [0u8; 16] && later_guid != [0u8; 16];
if neither_zero && base_guid != later_guid {
issues.push(EwfIntegrityAnomaly::SetIdentifierMismatch {
segment: idx.saturating_add(1),
});
}
}
}
} else if idx == 0 {
issues.push(EwfIntegrityAnomaly::VolumeSectionMissing);
}
let vol_count = if !multi && idx == 0 {
geometry.as_ref().map(|g| g.chunk_count)
} else {
None
};
let sectors_section = sections.iter().find(|s| s.type_name == "sectors");
let sectors_range = sectors_section.map(|s| {
(
s.offset.saturating_add(SECTION_DESCRIPTOR_SIZE as u64),
s.offset.saturating_add(s.size),
)
});
if sectors_section.is_none() {
issues.push(EwfIntegrityAnomaly::SectorsSectionMissing);
}
if let Some(table) = sections.iter().find(|s| s.type_name == "table") {
let data_start = (table.offset as usize).saturating_add(SECTION_DESCRIPTOR_SIZE);
if data.len() >= data_start.saturating_add(4) {
let count = le_u32(data, data_start);
total_table_entries = total_table_entries.saturating_add(count);
}
check_table_v1(
data,
table.offset,
vol_count,
file_size,
sectors_range,
&mut issues,
);
} else {
issues.push(EwfIntegrityAnomaly::TableSectionMissing);
}
if let (Some(t1), Some(t2)) = (
sections.iter().find(|s| s.type_name == "table"),
sections.iter().find(|s| s.type_name == "table2"),
) {
let b1_start = t1.offset.saturating_add(SECTION_DESCRIPTOR_SIZE as u64) as usize;
let b1_end = t1.offset.saturating_add(t1.size) as usize;
let b2_start = t2.offset.saturating_add(SECTION_DESCRIPTOR_SIZE as u64) as usize;
let b2_end = t2.offset.saturating_add(t2.size) as usize;
if let (Some(body1), Some(body2)) =
(data.get(b1_start..b1_end), data.get(b2_start..b2_end))
{
if body1.len() == body2.len() {
if let Some(offset) = body1.iter().zip(body2).position(|(a, b)| a != b) {
issues.push(EwfIntegrityAnomaly::Table2Mismatch { offset });
}
} else {
issues.push(EwfIntegrityAnomaly::Table2Mismatch { offset: 0 });
}
}
}
if let Some(e2) = sections.iter().find(|s| s.type_name == "error2") {
let body_start = e2.offset.saturating_add(SECTION_DESCRIPTOR_SIZE as u64) as usize;
if body_start.saturating_add(4) <= data.len() {
let count = le_u32(data, body_start);
if count > 0 {
issues.push(EwfIntegrityAnomaly::BadSectorsPresent { count });
}
}
}
if is_last && !sections.iter().any(|s| s.type_name == "done") {
issues.push(EwfIntegrityAnomaly::DoneSectionMissing);
}
all_sections.push(sections);
}
if multi {
if let Some(geom) = &geometry {
if total_table_entries != geom.chunk_count {
issues.push(EwfIntegrityAnomaly::TableChunkCountMismatch {
in_volume: geom.chunk_count,
in_table: total_table_entries,
});
}
}
}
if let Some(geom) = &geometry {
check_hash_all_segments(
&self.segments,
&all_sections,
geom,
self.expected_md5,
self.expected_sha1,
self.expected_sha256,
&mut issues,
&mut |_| {},
);
}
issues
}
fn analyse_all_ewf2(&self) -> Vec<EwfIntegrityAnomaly> {
self.analyse_all_ewf2_with_progress(|_| {})
}
fn analyse_all_ewf2_impl(
&self,
progress: &mut dyn FnMut(AnalysisProgress),
) -> Vec<EwfIntegrityAnomaly> {
let mut issues = Vec::new();
let n = self.segments.len();
let mut final_stored_md5: Option<[u8; 16]> = None;
let mut final_stored_sha1: Option<[u8; 20]> = None;
let mut final_stored_sha256: Option<[u8; 32]> = None;
for (idx, &data) in self.segments.iter().enumerate() {
let expected_seg_num = idx.saturating_add(1) as u32;
if data.len() < EVF2_FILE_HEADER_SIZE + EVF2_SECTION_DESCRIPTOR_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 = le_u32(data, 12);
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 compression_method = le_u16(data, 10);
if compression_method > 1 {
issues.push(EwfIntegrityAnomaly::UnsupportedCompressionAlgorithm {
method_id: compression_method,
});
}
let mut has_hash = false;
let mut has_media_info = false;
let mut chunk_table_body: Option<(usize, usize)> = None;
let mut stored_sector_md5: Option<[u8; 16]> = None;
let mut stored_sector_sha1: Option<[u8; 20]> = None;
let mut stored_sector_sha256: Option<[u8; 32]> = None;
let mut desc_offset = data.len().saturating_sub(EVF2_SECTION_DESCRIPTOR_SIZE);
loop {
if desc_offset.saturating_add(EVF2_SECTION_DESCRIPTOR_SIZE) > data.len()
|| desc_offset < EVF2_FILE_HEADER_SIZE
{
break;
}
let desc =
&data[desc_offset..desc_offset.saturating_add(EVF2_SECTION_DESCRIPTOR_SIZE)];
let section_type = le_u32(desc, 0);
let data_flags = le_u32(desc, 4);
let prev_offset = le_u64(desc, 8) as usize;
let data_size = le_u64(desc, 16) as usize;
let stored_hash: [u8; 16] = array_at(desc, 32);
let body_end = desc_offset;
let body_start = desc_offset.saturating_sub(data_size);
if data_flags & EVF2_DATA_FLAG_ENCRYPTED != 0 {
issues.push(EwfIntegrityAnomaly::Ewf2EncryptedSection {
offset: desc_offset 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: desc_offset as u64,
section_type_id: section_type,
computed,
stored: stored_hash,
});
}
}
}
match section_type {
EVF2_TYPE_MEDIA_INFO => {
has_media_info = true;
if let Some(body) = data.get(body_start..body_end) {
if !parse_media_info_body(body) {
issues.push(EwfIntegrityAnomaly::Ewf2MediaInfoParseFailed);
}
} else {
issues.push(EwfIntegrityAnomaly::Ewf2MediaInfoParseFailed);
}
}
EVF2_TYPE_CHUNK_TABLE => {
chunk_table_body = Some((body_start, body_end));
}
EVF2_TYPE_MD5_HASH => {
has_hash = true;
if data_size >= 16 {
if let Some(body) = data.get(body_start..body_end) {
let mut h = [0u8; 16];
h.copy_from_slice(&body[..16]);
stored_sector_md5 = Some(h);
}
}
}
EVF2_TYPE_SHA1_HASH => {
has_hash = true;
if data_size >= 20 {
if let Some(body) = data.get(body_start..body_end) {
let mut h = [0u8; 20];
h.copy_from_slice(&body[..20]);
stored_sector_sha1 = Some(h);
}
}
}
EVF2_TYPE_SHA256_HASH => {
has_hash = true;
if data_size >= 32 {
if let Some(body) = data.get(body_start..body_end) {
let mut h = [0u8; 32];
h.copy_from_slice(&body[..32]);
stored_sector_sha256 = Some(h);
}
}
}
_ => {}
}
}
if prev_offset == 0 || prev_offset >= desc_offset {
break;
}
desc_offset = prev_offset;
}
if idx == n.saturating_sub(1) && !has_hash {
issues.push(EwfIntegrityAnomaly::Ewf2HashSectionMissing);
}
if idx == 0 && !has_media_info {
issues.push(EwfIntegrityAnomaly::Ewf2MediaInfoMissing);
}
if idx == n.saturating_sub(1) {
final_stored_md5 = stored_sector_md5;
final_stored_sha1 = stored_sector_sha1;
final_stored_sha256 = stored_sector_sha256;
}
if let Some((ct_start, ct_end)) = chunk_table_body {
verify_ewf2_sector_data(
data,
ct_start,
ct_end,
None,
None,
None,
&mut issues,
progress,
);
}
}
if let Some(computed) = compute_hashes_ewf2(&self.segments) {
if let Some(stored) = final_stored_md5 {
if computed.md5 != stored {
issues.push(EwfIntegrityAnomaly::HashMismatch {
computed: computed.md5,
stored,
});
}
}
if let Some(stored) = final_stored_sha1 {
if computed.sha1 != stored {
issues.push(EwfIntegrityAnomaly::DigestSha1Mismatch {
computed: computed.sha1,
stored,
});
}
}
if let Some(stored) = final_stored_sha256 {
if computed.sha256 != stored {
issues.push(EwfIntegrityAnomaly::DigestSha256Mismatch {
computed: computed.sha256,
stored,
});
}
}
if let Some(expected) = self.expected_md5 {
if computed.md5 != expected {
issues.push(EwfIntegrityAnomaly::ExternalMd5Mismatch {
computed: computed.md5,
expected,
});
}
}
if let Some(expected) = self.expected_sha1 {
if computed.sha1 != expected {
issues.push(EwfIntegrityAnomaly::ExternalSha1Mismatch {
computed: computed.sha1,
expected,
});
}
}
if let Some(expected) = self.expected_sha256 {
if computed.sha256 != expected {
issues.push(EwfIntegrityAnomaly::ExternalSha256Mismatch {
computed: computed.sha256,
expected,
});
}
}
}
issues
}
fn analyse_all_ewf1_with_progress(
&self,
mut progress: impl FnMut(AnalysisProgress),
) -> 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);
let mut total_table_entries: u32 = 0;
for (idx, &data) in self.segments.iter().enumerate() {
let expected_seg_num = idx.saturating_add(1) as u16;
let is_last = idx == n.saturating_sub(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
&& data[0..8] != DVF_SIGNATURE
&& data[0..8] != LVF_SIGNATURE
{
issues.push(EwfIntegrityAnomaly::InvalidSignature);
}
let seg_num = le_u16(data, 9);
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 let Some(vol_sec) = sections
.iter()
.find(|s| s.type_name == "volume" || s.type_name == "disk")
{
if idx == 0 {
geometry = check_volume_v1(data, vol_sec.offset, vol_sec.size, &mut issues);
} else {
let later = check_volume_v1(data, vol_sec.offset, vol_sec.size, &mut issues);
if let (Some(ref base), Some(ref later_geom)) = (&geometry, &later) {
let base_guid = base.set_identifier;
let later_guid = later_geom.set_identifier;
if base_guid != [0u8; 16]
&& later_guid != [0u8; 16]
&& base_guid != later_guid
{
issues.push(EwfIntegrityAnomaly::SetIdentifierMismatch {
segment: idx.saturating_add(1),
});
}
}
}
} else if idx == 0 {
issues.push(EwfIntegrityAnomaly::VolumeSectionMissing);
}
let vol_count = if !multi && idx == 0 {
geometry.as_ref().map(|g| g.chunk_count)
} else {
None
};
let sectors_section = sections.iter().find(|s| s.type_name == "sectors");
let sectors_range = sectors_section.map(|s| {
(
s.offset.saturating_add(SECTION_DESCRIPTOR_SIZE as u64),
s.offset.saturating_add(s.size),
)
});
if sectors_section.is_none() {
issues.push(EwfIntegrityAnomaly::SectorsSectionMissing);
}
if let Some(table) = sections.iter().find(|s| s.type_name == "table") {
let data_start = (table.offset as usize).saturating_add(SECTION_DESCRIPTOR_SIZE);
if data.len() >= data_start.saturating_add(4) {
let count = le_u32(data, data_start);
total_table_entries = total_table_entries.saturating_add(count);
}
check_table_v1(
data,
table.offset,
vol_count,
file_size,
sectors_range,
&mut issues,
);
} else {
issues.push(EwfIntegrityAnomaly::TableSectionMissing);
}
if let (Some(t1), Some(t2)) = (
sections.iter().find(|s| s.type_name == "table"),
sections.iter().find(|s| s.type_name == "table2"),
) {
let b1_start = t1.offset.saturating_add(SECTION_DESCRIPTOR_SIZE as u64) as usize;
let b1_end = t1.offset.saturating_add(t1.size) as usize;
let b2_start = t2.offset.saturating_add(SECTION_DESCRIPTOR_SIZE as u64) as usize;
let b2_end = t2.offset.saturating_add(t2.size) as usize;
if let (Some(body1), Some(body2)) =
(data.get(b1_start..b1_end), data.get(b2_start..b2_end))
{
if body1.len() == body2.len() {
if let Some(offset) = body1.iter().zip(body2).position(|(a, b)| a != b) {
issues.push(EwfIntegrityAnomaly::Table2Mismatch { offset });
}
} else {
issues.push(EwfIntegrityAnomaly::Table2Mismatch { offset: 0 });
}
}
}
if let Some(e2) = sections.iter().find(|s| s.type_name == "error2") {
let body_start = e2.offset.saturating_add(SECTION_DESCRIPTOR_SIZE as u64) as usize;
if body_start.saturating_add(4) <= data.len() {
let count = le_u32(data, body_start);
if count > 0 {
issues.push(EwfIntegrityAnomaly::BadSectorsPresent { count });
}
}
}
if is_last && !sections.iter().any(|s| s.type_name == "done") {
issues.push(EwfIntegrityAnomaly::DoneSectionMissing);
}
all_sections.push(sections);
}
if multi {
if let Some(geom) = &geometry {
if total_table_entries != geom.chunk_count {
issues.push(EwfIntegrityAnomaly::TableChunkCountMismatch {
in_volume: geom.chunk_count,
in_table: total_table_entries,
});
}
}
}
if let Some(geom) = &geometry {
check_hash_all_segments(
&self.segments,
&all_sections,
geom,
self.expected_md5,
self.expected_sha1,
self.expected_sha256,
&mut issues,
&mut progress,
);
}
issues
}
fn analyse_all_ewf2_with_progress(
&self,
mut progress: impl FnMut(AnalysisProgress),
) -> Vec<EwfIntegrityAnomaly> {
self.analyse_all_ewf2_impl(&mut progress)
}
}
fn parse_header_section(data: &[u8]) -> Option<EwfHeaderMetadata> {
if data.len() < FILE_HEADER_SIZE + SECTION_DESCRIPTOR_SIZE {
return None;
}
let desc_off = FILE_HEADER_SIZE;
let desc = SectionDescriptor::parse(&data[desc_off..], desc_off as u64).ok()?;
if desc.section_type != "header" {
return None;
}
let section_size = desc.section_size as usize;
let body_start = desc_off.saturating_add(SECTION_DESCRIPTOR_SIZE);
let body_end = desc_off.saturating_add(section_size).min(data.len());
if body_start >= body_end {
return None;
}
let compressed = &data[body_start..body_end];
let mut decoder = ZlibDecoder::new(compressed);
let mut text = String::new();
decoder.read_to_string(&mut text).ok()?;
parse_header_text(&text)
}
fn parse_header_text(text: &str) -> Option<EwfHeaderMetadata> {
let lines: Vec<&str> = text
.lines()
.map(|l| l.trim_end_matches('\r'))
.filter(|l| !l.is_empty())
.collect();
if lines.len() < 4 {
return None;
}
let keys: Vec<&str> = lines[2].split('\t').collect();
let vals: Vec<&str> = lines[3].split('\t').collect();
let mut meta = EwfHeaderMetadata {
description: String::new(),
case_number: String::new(),
evidence_number: String::new(),
examiner_name: String::new(),
acquisition_date: String::new(),
system_date: String::new(),
password_hash: String::new(),
acquisition_software: String::new(),
};
for (i, &key) in keys.iter().enumerate() {
let val = vals.get(i).copied().unwrap_or("").to_owned();
match key {
"a" => meta.description = val,
"c" => meta.case_number = val,
"e" => meta.evidence_number = val,
"t" => meta.examiner_name = val,
"m" => meta.acquisition_date = val,
"u" => meta.system_date = val,
"p" => meta.password_hash = val,
"r" => meta.acquisition_software = val,
_ => {}
}
}
Some(meta)
}
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,
set_identifier: [u8; 16],
}
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.saturating_add(SECTION_DESCRIPTOR_SIZE) > data.len() {
break;
}
let raw = &data[off..off.saturating_add(SECTION_DESCRIPTOR_SIZE)];
let Ok(desc) = SectionDescriptor::parse(raw, pos) else {
break;
};
let crc_ok = desc.verify_crc(raw);
let stored_crc = desc.stored_crc;
let next = desc.next;
let section_size = desc.section_size;
let type_name = desc.section_type;
if !crc_ok {
issues.push(EwfIntegrityAnomaly::SectionDescriptorCrcMismatch {
offset: pos,
section_type: type_name.clone(),
computed: adler32(&raw[SectionDescriptor::crc_covers()]),
stored: stored_crc,
});
}
if !KNOWN_TYPES.contains(&type_name.as_str()) {
issues.push(EwfIntegrityAnomaly::UnknownSectionType {
offset: pos,
type_name: type_name.clone(),
});
}
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.saturating_sub(section_end);
let non_zero = data
.get(section_end as usize..next as usize)
.is_some_and(|s| s.iter().any(|&b| b != 0));
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,
section_size: u64,
issues: &mut Vec<EwfIntegrityAnomaly>,
) -> Option<VolumeGeometry> {
let data_start = (desc_offset as usize).saturating_add(SECTION_DESCRIPTOR_SIZE);
if data.len() < data_start.saturating_add(VOLUME_DATA_MIN) {
return None;
}
let body_len = (section_size as usize).saturating_sub(SECTION_DESCRIPTOR_SIZE);
let vol_end = data_start.saturating_add(body_len).min(data.len());
let vol = &data[data_start..vol_end];
let parsed = EwfVolume::parse(vol).ok()?;
let chunk_count = parsed.chunk_count;
let sectors_per_chunk = parsed.sectors_per_chunk;
let bytes_per_sector = parsed.bytes_per_sector;
let sector_count = parsed.sector_count;
if !VALID_MEDIA_TYPES.contains(&parsed.media_type) {
issues.push(EwfIntegrityAnomaly::MediaTypeUnknown {
media_type: parsed.media_type,
});
}
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).saturating_mul(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,
});
}
}
let set_identifier = parsed.set_identifier;
if parsed.verify_crc(vol) == Some(false) {
issues.push(EwfIntegrityAnomaly::VolumeBodyCrcMismatch {
computed: adler32(&vol[EwfVolume::crc_covers()]),
stored: parsed.stored_crc.unwrap_or(0),
});
}
Some(VolumeGeometry {
chunk_count,
sectors_per_chunk,
bytes_per_sector,
sector_count,
set_identifier,
})
}
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).saturating_add(SECTION_DESCRIPTOR_SIZE);
if data.len() < data_start.saturating_add(TABLE_HEADER_SIZE) {
return;
}
let tbl = &data[data_start..];
let Ok(header) = TableHeader::parse(tbl) else {
return;
};
let entry_count = header.entry_count;
let base_offset = header.base_offset;
if header.verify_crc(tbl) == Some(false) {
issues.push(EwfIntegrityAnomaly::TableHeaderAdler32Mismatch {
computed: adler32(&tbl[TableHeader::crc_covers()]),
stored: header.stored_crc,
});
}
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.saturating_add(TABLE_HEADER_SIZE);
for i in 0..entry_count {
let entry_off = entries_start.saturating_add((i as usize).saturating_mul(4));
let Some(entry_bytes) = data.get(entry_off..entry_off.saturating_add(4)) else {
break;
};
let Ok(entry) = sections::TableEntry::parse(entry_bytes) else {
break;
};
let chunk_rel = u64::from(entry.chunk_offset);
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).saturating_add(SECTION_DESCRIPTOR_SIZE);
if data.len() < tbl_data_start.saturating_add(TABLE_HEADER_SIZE) {
return Vec::new();
}
let tbl = &data[tbl_data_start..];
let Ok(header) = TableHeader::parse(tbl) else {
return Vec::new();
};
let entry_count = header.entry_count as usize;
let base_offset = header.base_offset as usize;
let entries_start = tbl_data_start.saturating_add(TABLE_HEADER_SIZE);
let sectors_body_end = sectors.offset.saturating_add(sectors.size) as usize;
let entry_at = |idx: usize| -> Option<(bool, usize)> {
let off = entries_start.saturating_add(idx.saturating_mul(4));
let bytes = data.get(off..off.saturating_add(4))?;
let e = sections::TableEntry::parse(bytes).ok()?;
Some((e.compressed, e.chunk_offset as usize))
};
let mut chunks = Vec::with_capacity(entry_count.min(data.len()));
for i in 0..entry_count {
let Some((compressed, rel)) = entry_at(i) else {
break;
};
let start = base_offset.saturating_add(rel);
let end = if i.saturating_add(1) < entry_count {
let Some((_, next_rel)) = entry_at(i.saturating_add(1)) else {
break;
};
base_offset.saturating_add(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]>,
expected_sha256: Option<[u8; 32]>,
issues: &mut Vec<EwfIntegrityAnomaly>,
progress: &mut dyn FnMut(AnalysisProgress),
) {
let chunk_size =
u64::from(geom.sectors_per_chunk).saturating_mul(u64::from(geom.bytes_per_sector));
let total_bytes = geom
.sector_count
.saturating_mul(u64::from(geom.bytes_per_sector));
let mut bytes_remaining = total_bytes;
let mut md5_h = Md5::new();
let mut sha1_h = Sha1::new();
let mut sha256_h = Sha256::new();
let chunk_size_usize = chunk_size as usize;
let mut global_chunk_idx: usize = 0;
'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];
let this_chunk_idx = global_chunk_idx;
global_chunk_idx = global_chunk_idx.saturating_add(1);
let has_uncompressed_checksum = !compressed && (raw.len() > chunk_size_usize);
if has_uncompressed_checksum && raw.len() >= chunk_size_usize.saturating_add(4) {
let crc_end = chunk_size_usize;
let stored = le_u32(raw, crc_end);
let computed = adler32(&raw[..crc_end]);
if computed != stored {
issues.push(EwfIntegrityAnomaly::ChunkChecksumMismatch {
chunk_index: this_chunk_idx,
computed,
stored,
});
}
}
if compressed {
let limit = (to_hash as u64).saturating_add(1);
let mut decompressed = Vec::with_capacity(to_hash.min(raw.len()));
if ZlibDecoder::new(raw)
.take(limit)
.read_to_end(&mut decompressed)
.is_err()
{
issues.push(EwfIntegrityAnomaly::ChunkDecompressionError {
chunk_index: this_chunk_idx,
});
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);
sha256_h.update(slice);
} else {
let slice = &raw[..raw.len().min(to_hash)];
md5_h.update(slice);
sha1_h.update(slice);
sha256_h.update(slice);
}
bytes_remaining = bytes_remaining.saturating_sub(to_hash as u64);
progress(AnalysisProgress {
chunks_done: global_chunk_idx,
chunks_total: None,
bytes_done: total_bytes.saturating_sub(bytes_remaining),
});
}
}
let computed_md5: [u8; 16] = md5_h.finalize().into();
let computed_sha1: [u8; 20] = sha1_h.finalize().into();
let computed_sha256: [u8; 32] = sha256_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).saturating_add(SECTION_DESCRIPTOR_SIZE);
if let Some(stored_slice) = last_data.get(body_start..body_start.saturating_add(16)) {
let stored: [u8; 16] = stored_slice.try_into().unwrap_or([0u8; 16]);
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).saturating_add(SECTION_DESCRIPTOR_SIZE);
if let Some(sha1_slice) =
last_data.get(body_start.saturating_add(16)..body_start.saturating_add(36))
{
let stored: [u8; 20] = sha1_slice.try_into().unwrap_or([0u8; 20]);
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,
});
}
}
if let Some(expected) = expected_sha256 {
if computed_sha256 != expected {
issues.push(EwfIntegrityAnomaly::ExternalSha256Mismatch {
computed: computed_sha256,
expected,
});
}
}
}
fn parse_media_info_body(body: &[u8]) -> bool {
if body.is_empty() {
return false;
}
let mut decompressed = Vec::new();
if ZlibDecoder::new(body)
.read_to_end(&mut decompressed)
.is_err()
{
return false;
}
let text_bytes = if decompressed.starts_with(&[0xFF, 0xFE]) {
&decompressed[2..]
} else {
&decompressed[..]
};
if text_bytes.len() % 2 != 0 {
return false;
}
let units: Vec<u16> = text_bytes
.chunks_exact(2)
.map(|b| u16::from_le_bytes([b[0], b[1]]))
.collect();
String::from_utf16(&units).is_ok()
}
fn verify_ewf2_sector_data(
data: &[u8],
ct_start: usize,
ct_end: usize,
stored_md5: Option<[u8; 16]>,
stored_sha1: Option<[u8; 20]>,
stored_sha256: Option<[u8; 32]>,
issues: &mut Vec<EwfIntegrityAnomaly>,
progress: &mut dyn FnMut(AnalysisProgress),
) -> Option<ComputedHashes> {
let tbl = data.get(ct_start..ct_end)?;
if tbl.len() < EVF2_CHUNK_TABLE_HEADER_SIZE + EVF2_CHUNK_TABLE_ENTRY_SIZE {
return None;
}
let chunk_count = le_u64(tbl, 8) as usize;
let checksum_off = EVF2_CHUNK_TABLE_HEADER_SIZE
.saturating_add(chunk_count.saturating_mul(EVF2_CHUNK_TABLE_ENTRY_SIZE));
if checksum_off.saturating_add(4) <= tbl.len() {
let computed_cs = adler32(&tbl[EVF2_CHUNK_TABLE_HEADER_SIZE..checksum_off]);
let stored_cs = le_u32(tbl, checksum_off);
if computed_cs != stored_cs {
issues.push(EwfIntegrityAnomaly::Ewf2ChunkTableChecksumMismatch {
computed: computed_cs,
stored: stored_cs,
});
}
}
let mut md5_h = Md5::new();
let mut sha1_h = Sha1::new();
let mut sha256_h = Sha256::new();
for i in 0..chunk_count {
let entry_off = EVF2_CHUNK_TABLE_HEADER_SIZE
.saturating_add(i.saturating_mul(EVF2_CHUNK_TABLE_ENTRY_SIZE));
if entry_off.saturating_add(EVF2_CHUNK_TABLE_ENTRY_SIZE) > tbl.len() {
break;
}
let file_offset = le_u64(tbl, entry_off) as usize;
let chunk_data_size = le_u32(tbl, entry_off.saturating_add(8)) as usize;
let flags = le_u32(tbl, entry_off.saturating_add(12));
let raw_size = chunk_data_size.saturating_sub(4);
let chunk_raw = match data.get(file_offset..file_offset.saturating_add(raw_size)) {
Some(r) => r,
None => break,
};
if chunk_data_size >= 4 {
let crc_start = file_offset.saturating_add(raw_size);
if let Some(crc_bytes) = data.get(crc_start..crc_start.saturating_add(4)) {
let stored_crc = u32::from_le_bytes(crc_bytes.try_into().unwrap_or([0u8; 4]));
let computed_crc = adler32(chunk_raw);
if computed_crc != stored_crc {
issues.push(EwfIntegrityAnomaly::ChunkChecksumMismatch {
chunk_index: i,
computed: computed_crc,
stored: stored_crc,
});
}
}
}
if flags & EVF2_CHUNK_FLAG_COMPRESSED != 0 {
let mut decompressed = Vec::with_capacity(raw_size.min(chunk_raw.len()));
if ZlibDecoder::new(chunk_raw)
.read_to_end(&mut decompressed)
.is_err()
{
issues.push(EwfIntegrityAnomaly::ChunkDecompressionError { chunk_index: i });
continue;
}
md5_h.update(&decompressed);
sha1_h.update(&decompressed);
sha256_h.update(&decompressed);
} else {
md5_h.update(chunk_raw);
sha1_h.update(chunk_raw);
sha256_h.update(chunk_raw);
}
progress(AnalysisProgress {
chunks_done: i.saturating_add(1),
chunks_total: Some(chunk_count),
bytes_done: i.saturating_add(1).saturating_mul(raw_size) as u64,
});
}
let computed_md5: [u8; 16] = md5_h.finalize().into();
let computed_sha1: [u8; 20] = sha1_h.finalize().into();
let computed_sha256: [u8; 32] = sha256_h.finalize().into();
if let Some(stored) = stored_md5 {
if computed_md5 != stored {
issues.push(EwfIntegrityAnomaly::HashMismatch {
computed: computed_md5,
stored,
});
}
}
if let Some(stored) = stored_sha1 {
if computed_sha1 != stored {
issues.push(EwfIntegrityAnomaly::DigestSha1Mismatch {
computed: computed_sha1,
stored,
});
}
}
if let Some(stored) = stored_sha256 {
if computed_sha256 != stored {
issues.push(EwfIntegrityAnomaly::DigestSha256Mismatch {
computed: computed_sha256,
stored,
});
}
}
Some(ComputedHashes {
md5: computed_md5,
sha1: computed_sha1,
sha256: computed_sha256,
})
}
fn compute_hashes_ewf2(segments: &[&[u8]]) -> Option<ComputedHashes> {
let mut md5_h = Md5::new();
let mut sha1_h = Sha1::new();
let mut sha256_h = Sha256::new();
let mut found_chunks = false;
for &data in segments {
if data.len() < EVF2_FILE_HEADER_SIZE + EVF2_SECTION_DESCRIPTOR_SIZE {
continue;
}
let mut desc_offset = data.len().saturating_sub(EVF2_SECTION_DESCRIPTOR_SIZE);
let mut chunk_table_body: Option<(usize, usize)> = None;
loop {
if desc_offset.saturating_add(EVF2_SECTION_DESCRIPTOR_SIZE) > data.len()
|| desc_offset < EVF2_FILE_HEADER_SIZE
{
break;
}
let desc = &data[desc_offset..desc_offset.saturating_add(EVF2_SECTION_DESCRIPTOR_SIZE)];
let section_type = le_u32(desc, 0);
let data_flags = le_u32(desc, 4);
let prev_offset = le_u64(desc, 8) as usize;
let data_size = le_u64(desc, 16) as usize;
let body_end = desc_offset;
let body_start = desc_offset.saturating_sub(data_size);
if data_flags & EVF2_DATA_FLAG_ENCRYPTED == 0 && section_type == EVF2_TYPE_CHUNK_TABLE {
chunk_table_body = Some((body_start, body_end));
}
if prev_offset == 0 || prev_offset >= desc_offset {
break;
}
desc_offset = prev_offset;
}
let (ct_start, ct_end) = match chunk_table_body {
Some(b) => b,
None => continue,
};
let tbl = match data.get(ct_start..ct_end) {
Some(t) => t,
None => continue,
};
if tbl.len() < EVF2_CHUNK_TABLE_HEADER_SIZE + EVF2_CHUNK_TABLE_ENTRY_SIZE {
continue;
}
let chunk_count = le_u64(tbl, 8) as usize;
for i in 0..chunk_count {
let entry_off = EVF2_CHUNK_TABLE_HEADER_SIZE
.saturating_add(i.saturating_mul(EVF2_CHUNK_TABLE_ENTRY_SIZE));
if entry_off.saturating_add(EVF2_CHUNK_TABLE_ENTRY_SIZE) > tbl.len() {
break;
}
let file_offset = le_u64(tbl, entry_off) as usize;
let chunk_data_size = le_u32(tbl, entry_off.saturating_add(8)) as usize;
let flags = le_u32(tbl, entry_off.saturating_add(12));
let raw_size = chunk_data_size.saturating_sub(4);
let chunk_raw = match data.get(file_offset..file_offset.saturating_add(raw_size)) {
Some(r) => r,
None => break,
};
if flags & EVF2_CHUNK_FLAG_COMPRESSED != 0 {
let mut decompressed = Vec::with_capacity(raw_size.min(chunk_raw.len()));
if ZlibDecoder::new(chunk_raw)
.read_to_end(&mut decompressed)
.is_err()
{
continue;
}
md5_h.update(&decompressed);
sha1_h.update(&decompressed);
sha256_h.update(&decompressed);
} else {
md5_h.update(chunk_raw);
sha1_h.update(chunk_raw);
sha256_h.update(chunk_raw);
}
found_chunks = true;
}
}
if !found_chunks {
return None;
}
Some(ComputedHashes {
md5: md5_h.finalize().into(),
sha1: sha1_h.finalize().into(),
sha256: sha256_h.finalize().into(),
})
}
fn compute_hashes_ewf1(segments: &[&[u8]]) -> Option<ComputedHashes> {
let first = segments.first().copied()?;
if first.len() < FILE_HEADER_SIZE {
return None;
}
if first[0..8] != EVF_SIGNATURE && first[0..8] != DVF_SIGNATURE && first[0..8] != LVF_SIGNATURE
{
return None;
}
let mut dummy = Vec::new();
let sections_first = walk_sections_v1(first, &mut dummy);
let vol_sec = sections_first
.iter()
.find(|s| s.type_name == "volume" || s.type_name == "disk")?;
let geom = check_volume_v1(first, vol_sec.offset, vol_sec.size, &mut dummy)?;
let chunk_size =
u64::from(geom.sectors_per_chunk).saturating_mul(u64::from(geom.bytes_per_sector));
let total_bytes = geom
.sector_count
.saturating_mul(u64::from(geom.bytes_per_sector));
let mut bytes_remaining = total_bytes;
let mut md5_h = Md5::new();
let mut sha1_h = Sha1::new();
let mut sha256_h = Sha256::new();
let mut all_sections: Vec<Vec<Section>> = Vec::new();
for &seg in segments {
let mut d = Vec::new();
all_sections.push(walk_sections_v1(seg, &mut d));
}
'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.min(raw.len()));
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);
sha256_h.update(slice);
} else {
let slice = &raw[..raw.len().min(to_hash)];
md5_h.update(slice);
sha1_h.update(slice);
sha256_h.update(slice);
}
bytes_remaining = bytes_remaining.saturating_sub(to_hash as u64);
}
}
Some(ComputedHashes {
md5: md5_h.finalize().into(),
sha1: sha1_h.finalize().into(),
sha256: sha256_h.finalize().into(),
})
}
pub(crate) fn adler32(data: &[u8]) -> u32 {
sections::adler32(data)
}
impl EwfIntegrityAnomaly {
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Self::InvalidSignature => "EWF-INVALID-SIGNATURE",
Self::SegmentNumberZero => "EWF-SEGMENT-NUMBER-ZERO",
Self::SectionDescriptorCrcMismatch { .. } => "EWF-SECTION-DESCRIPTOR-CRC-MISMATCH",
Self::SectionChainBroken { .. } => "EWF-SECTION-CHAIN-BROKEN",
Self::SectionGapNonZero { .. } => "EWF-SECTION-GAP-NON-ZERO",
Self::VolumeSectionMissing => "EWF-VOLUME-SECTION-MISSING",
Self::UnknownSectionType { .. } => "EWF-UNKNOWN-SECTION-TYPE",
Self::DoneSectionMissing => "EWF-DONE-SECTION-MISSING",
Self::SectorsSectionMissing => "EWF-SECTORS-SECTION-MISSING",
Self::TableSectionMissing => "EWF-TABLE-SECTION-MISSING",
Self::ChunkSizeInvalid { .. } => "EWF-CHUNK-SIZE-INVALID",
Self::SectorCountMismatch { .. } => "EWF-SECTOR-COUNT-MISMATCH",
Self::BytesPerSectorInvalid { .. } => "EWF-BYTES-PER-SECTOR-INVALID",
Self::TableChunkCountMismatch { .. } => "EWF-TABLE-CHUNK-COUNT-MISMATCH",
Self::TableHeaderAdler32Mismatch { .. } => "EWF-TABLE-HEADER-ADLER32-MISMATCH",
Self::TableEntryOutOfBounds { .. } => "EWF-TABLE-ENTRY-OUT-OF-BOUNDS",
Self::TableEntryOutsideSectorsRange { .. } => "EWF-TABLE-ENTRY-OUTSIDE-SECTORS-RANGE",
Self::SectionGapZero { .. } => "EWF-SECTION-GAP-ZERO",
Self::HashMismatch { .. } => "EWF-HASH-MISMATCH",
Self::HashSectionMissing => "EWF-HASH-SECTION-MISSING",
Self::Table2Mismatch { .. } => "EWF-TABLE2-MISMATCH",
Self::BadSectorsPresent { .. } => "EWF-BAD-SECTORS-PRESENT",
Self::SegmentOutOfOrder { .. } => "EWF-SEGMENT-OUT-OF-ORDER",
Self::DigestSha1Mismatch { .. } => "EWF-DIGEST-SHA1-MISMATCH",
Self::DigestSha256Mismatch { .. } => "EWF-DIGEST-SHA256-MISMATCH",
Self::ExternalMd5Mismatch { .. } => "EWF-EXTERNAL-MD5-MISMATCH",
Self::ExternalSha1Mismatch { .. } => "EWF-EXTERNAL-SHA1-MISMATCH",
Self::Ewf2SectionDataHashMismatch { .. } => "EWF-EWF2-SECTION-DATA-HASH-MISMATCH",
Self::Ewf2EncryptedSection { .. } => "EWF-EWF2-ENCRYPTED-SECTION",
Self::Ewf2HashSectionMissing => "EWF-EWF2-HASH-SECTION-MISSING",
Self::VolumeBodyCrcMismatch { .. } => "EWF-VOLUME-BODY-CRC-MISMATCH",
Self::MediaTypeUnknown { .. } => "EWF-MEDIA-TYPE-UNKNOWN",
Self::SetIdentifierMismatch { .. } => "EWF-SET-IDENTIFIER-MISMATCH",
Self::Ewf2MediaInfoMissing => "EWF-EWF2-MEDIA-INFO-MISSING",
Self::Ewf2ChunkTableChecksumMismatch { .. } => "EWF-EWF2-CHUNK-TABLE-CHECKSUM-MISMATCH",
Self::ChunkChecksumMismatch { .. } => "EWF-CHUNK-CHECKSUM-MISMATCH",
Self::ChunkDecompressionError { .. } => "EWF-CHUNK-DECOMPRESSION-ERROR",
Self::UnsupportedCompressionAlgorithm { .. } => "EWF-UNSUPPORTED-COMPRESSION-ALGORITHM",
Self::ExternalSha256Mismatch { .. } => "EWF-EXTERNAL-SHA256-MISMATCH",
Self::Ewf2MediaInfoParseFailed => "EWF-EWF2-MEDIA-INFO-PARSE-FAILED",
}
}
}
impl forensicnomicon::report::Observation for EwfIntegrityAnomaly {
fn severity(&self) -> Option<Severity> {
Some(self.severity())
}
fn code(&self) -> &'static str {
self.code()
}
fn note(&self) -> String {
self.to_string()
}
}
#[cfg(test)]
mod adler32_tests {
use super::adler32;
#[test]
fn adler32_matches_published_vectors() {
assert_eq!(adler32(b""), 0x0000_0001);
assert_eq!(adler32(b"abc"), 0x024D_0127);
assert_eq!(adler32(b"Wikipedia"), 0x11E6_0398);
}
}