/// XEX2 security info parsing. /// /// The security info structure is located at the file offset specified by /// `xex2_header.security_offset` and contains cryptographic signatures, /// encryption keys, memory layout information, and per-page integrity digests. use std::fmt; use crate::error::{Result, Xex2Error}; use crate::util::{read_bytes, read_u32_be}; // ── Security Info ───────────────────────────────────────────────────────────── /// The parsed XEX2 security info structure. #[derive(Debug, Clone)] pub struct SecurityInfo { /// Size of this security info structure in bytes. pub header_size: u32, /// Size of the decompressed PE image in bytes. pub image_size: u32, /// RSA-2048 signature over the header (256 bytes). pub rsa_signature: [u8; 256], /// Unknown field at offset 0x108 (often a length value). pub unk_108: u32, /// Image flags bitmask. pub image_flags: ImageFlags, /// Virtual memory address where the PE image is loaded. pub load_address: u32, /// SHA-1 digest of section data (20 bytes). pub section_digest: [u8; 20], /// Number of import table entries. pub import_table_count: u32, /// SHA-1 digest of the import table (20 bytes). pub import_table_digest: [u8; 20], /// XGD2 media identifier (16 bytes). pub xgd2_media_id: [u8; 16], /// Encrypted AES-128 session key (16 bytes). pub aes_key: [u8; 16], /// Memory address of the XEX export table (0 if none). pub export_table: u32, /// SHA-1 digest of header data (20 bytes). pub header_digest: [u8; 20], /// Allowed regions bitmask. pub region: RegionFlags, /// Allowed media types bitmask. pub allowed_media_types: MediaFlags, /// Number of page descriptors following. pub page_descriptor_count: u32, /// Per-page descriptors with section types and integrity digests. pub page_descriptors: Vec, } /// Parses the security info structure from `data` at the given file `offset`. pub fn parse_security_info(data: &[u8], offset: u32) -> Result { let off = offset as usize; // Minimum size: fixed fields up to page_descriptor_count (0x184 bytes) let min_size = off + 0x184; if min_size > data.len() { return Err(Xex2Error::InvalidOffset { name: "security_info", offset, file_size: data.len(), }); } let header_size = read_u32_be(data, off)?; let image_size = read_u32_be(data, off + 0x004)?; let mut rsa_signature = [0u8; 256]; rsa_signature.copy_from_slice(read_bytes(data, off + 0x008, 256)?); let unk_108 = read_u32_be(data, off + 0x108)?; let image_flags = ImageFlags(read_u32_be(data, off + 0x10C)?); let load_address = read_u32_be(data, off + 0x110)?; let mut section_digest = [0u8; 20]; section_digest.copy_from_slice(read_bytes(data, off + 0x114, 20)?); let import_table_count = read_u32_be(data, off + 0x128)?; let mut import_table_digest = [0u8; 20]; import_table_digest.copy_from_slice(read_bytes(data, off + 0x12C, 20)?); let mut xgd2_media_id = [0u8; 16]; xgd2_media_id.copy_from_slice(read_bytes(data, off + 0x140, 16)?); let mut aes_key = [0u8; 16]; aes_key.copy_from_slice(read_bytes(data, off + 0x150, 16)?); let export_table = read_u32_be(data, off + 0x160)?; let mut header_digest = [0u8; 20]; header_digest.copy_from_slice(read_bytes(data, off + 0x164, 20)?); let region = RegionFlags(read_u32_be(data, off + 0x178)?); let allowed_media_types = MediaFlags(read_u32_be(data, off + 0x17C)?); let page_descriptor_count = read_u32_be(data, off + 0x180)?; // Parse page descriptors (24 bytes each, starting at offset + 0x184) let desc_start = off + 0x184; let desc_total = page_descriptor_count as usize * 24; if desc_start + desc_total > data.len() { return Err(Xex2Error::FileTooSmall { expected: desc_start + desc_total, actual: data.len(), }); } let mut page_descriptors = Vec::with_capacity(page_descriptor_count as usize); for i in 0..page_descriptor_count as usize { let base = desc_start + i * 24; let value = read_u32_be(data, base)?; let info = ((value >> 28) & 0xF) as u8; let page_count = value & 0x0FFFFFFF; let mut data_digest = [0u8; 20]; data_digest.copy_from_slice(read_bytes(data, base + 4, 20)?); page_descriptors.push(PageDescriptor { section_type: SectionType::from_raw(info), page_count, data_digest, }); } Ok(SecurityInfo { header_size, image_size, rsa_signature, unk_108, image_flags, load_address, section_digest, import_table_count, import_table_digest, xgd2_media_id, aes_key, export_table, header_digest, region, allowed_media_types, page_descriptor_count, page_descriptors, }) } // ── Image Flags ─────────────────────────────────────────────────────────────── /// Image flags bitmask from the security info. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ImageFlags(pub u32); impl ImageFlags { const FLAGS: &[(u32, &str)] = &[ (0x00000002, "MANUFACTURING_UTILITY"), (0x00000004, "MANUFACTURING_SUPPORT_TOOLS"), (0x00000008, "XGD2_MEDIA_ONLY"), (0x00000100, "CARDEA_KEY"), (0x00000200, "XEIKA_KEY"), (0x00000400, "USERMODE_TITLE"), (0x00000800, "USERMODE_SYSTEM"), (0x10000000, "PAGE_SIZE_4KB"), (0x20000000, "REGION_FREE"), (0x40000000, "REVOCATION_CHECK_OPTIONAL"), (0x80000000, "REVOCATION_CHECK_REQUIRED"), ]; /// Returns a list of human-readable flag names that are set. pub fn flag_names(self) -> Vec<&'static str> { Self::FLAGS .iter() .filter(|(bit, _)| self.0 & bit != 0) .map(|(_, name)| *name) .collect() } /// Returns `true` if the 4KB page size flag is set (otherwise 64KB). pub fn is_4kb_pages(self) -> bool { self.0 & 0x10000000 != 0 } /// Returns the page size in bytes based on the page size flag. pub fn page_size(self) -> u32 { if self.is_4kb_pages() { 0x1000 } else { 0x10000 } } } impl fmt::Display for ImageFlags { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let names = self.flag_names(); if names.is_empty() { write!(f, "0x{:08X}", self.0) } else { write!(f, "0x{:08X} [{}]", self.0, names.join(", ")) } } } // ── Region Flags ────────────────────────────────────────────────────────────── /// Allowed region flags bitmask. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct RegionFlags(pub u32); impl RegionFlags { const NTSCU: u32 = 0x000000FF; const NTSCJ: u32 = 0x0000FF00; const NTSCJ_JAPAN: u32 = 0x00000100; const NTSCJ_CHINA: u32 = 0x00000200; const PAL: u32 = 0x00FF0000; const PAL_AU_NZ: u32 = 0x00010000; const OTHER: u32 = 0xFF000000; const ALL: u32 = 0xFFFFFFFF; /// Returns a human-readable description of the active region flags. pub fn description(self) -> String { if self.0 == Self::ALL { return "ALL REGIONS".to_string(); } if self.0 == 0 { return "NONE".to_string(); } let mut regions = Vec::new(); if self.0 & Self::NTSCU != 0 { regions.push("NTSC/U"); } if self.0 & Self::NTSCJ_JAPAN != 0 { regions.push("NTSC/J-Japan"); } if self.0 & Self::NTSCJ_CHINA != 0 { regions.push("NTSC/J-China"); } // Only show generic NTSC/J if specific bits aren't set but region is if self.0 & Self::NTSCJ != 0 && self.0 & Self::NTSCJ_JAPAN == 0 && self.0 & Self::NTSCJ_CHINA == 0 { regions.push("NTSC/J"); } if self.0 & Self::PAL_AU_NZ != 0 { regions.push("PAL-AU/NZ"); } // Only show generic PAL if specific bits aren't set but region is if self.0 & Self::PAL != 0 && self.0 & Self::PAL_AU_NZ == 0 { regions.push("PAL"); } if self.0 & Self::OTHER != 0 { regions.push("Other"); } regions.join(", ") } } impl fmt::Display for RegionFlags { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "0x{:08X} [{}]", self.0, self.description()) } } // ── Media Flags ─────────────────────────────────────────────────────────────── /// Allowed media types bitmask. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct MediaFlags(pub u32); impl MediaFlags { const FLAGS: &[(u32, &str)] = &[ (0x00000001, "HARD_DISK"), (0x00000002, "DVD_X2"), (0x00000004, "DVD_CD"), (0x00000008, "DVD_5"), (0x00000010, "DVD_9"), (0x00000020, "SYSTEM_FLASH"), (0x00000080, "MEMORY_UNIT"), (0x00000100, "USB_MASS_STORAGE"), (0x00000200, "NETWORK"), (0x00000400, "DIRECT_FROM_MEMORY"), (0x00000800, "RAM_DRIVE"), (0x00001000, "SVOD"), (0x01000000, "INSECURE_PACKAGE"), (0x02000000, "SAVEGAME_PACKAGE"), (0x04000000, "LOCALLY_SIGNED_PACKAGE"), (0x08000000, "LIVE_SIGNED_PACKAGE"), (0x10000000, "XBOX_PACKAGE"), ]; /// Returns a list of human-readable flag names that are set. pub fn flag_names(self) -> Vec<&'static str> { Self::FLAGS .iter() .filter(|(bit, _)| self.0 & bit != 0) .map(|(_, name)| *name) .collect() } } impl fmt::Display for MediaFlags { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let names = self.flag_names(); if names.is_empty() { write!(f, "0x{:08X}", self.0) } else { write!(f, "0x{:08X} [{}]", self.0, names.join(", ")) } } } // ── Page Descriptor ─────────────────────────────────────────────────────────── /// A single page descriptor with section type, page count, and SHA-1 digest. #[derive(Debug, Clone)] pub struct PageDescriptor { /// The section type (code, data, read-only data, or unknown). pub section_type: SectionType, /// Number of pages in this section. pub page_count: u32, /// SHA-1 hash of the page data (20 bytes). pub data_digest: [u8; 20], } /// Section type from the page descriptor info bits. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SectionType { Code, Data, ReadOnlyData, Unknown(u8), } impl SectionType { /// Creates a `SectionType` from the raw 4-bit info field. pub fn from_raw(raw: u8) -> Self { match raw { 1 => Self::Code, 2 => Self::Data, 3 => Self::ReadOnlyData, v => Self::Unknown(v), } } } impl fmt::Display for SectionType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Code => write!(f, "Code"), Self::Data => write!(f, "Data"), Self::ReadOnlyData => write!(f, "ReadOnly"), Self::Unknown(v) => write!(f, "Unknown({v})"), } } } #[cfg(test)] mod tests { use super::*; fn sample_data() -> Vec { let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR")); std::fs::read(&path).expect("sample file should exist") } #[test] fn test_parse_security_info_header() { let data = sample_data(); let sec = parse_security_info(&data, 0x90).unwrap(); assert_eq!(sec.header_size, 0x00000F34); assert_eq!(sec.image_size, 0x00920000); } #[test] fn test_parse_security_info_fields() { let data = sample_data(); let sec = parse_security_info(&data, 0x90).unwrap(); assert_eq!(sec.unk_108, 0x00000174); assert_eq!(sec.image_flags, ImageFlags(0x00000008)); assert_eq!(sec.load_address, 0x82000000); assert_eq!(sec.import_table_count, 2); assert_eq!(sec.export_table, 0x00000000); } #[test] fn test_parse_security_region_flags() { let data = sample_data(); let sec = parse_security_info(&data, 0x90).unwrap(); assert_eq!(sec.region, RegionFlags(0xFFFFFFFF)); assert_eq!(sec.region.description(), "ALL REGIONS"); } #[test] fn test_parse_security_media_flags() { let data = sample_data(); let sec = parse_security_info(&data, 0x90).unwrap(); assert_eq!(sec.allowed_media_types, MediaFlags(0x00000004)); assert_eq!(sec.allowed_media_types.flag_names(), vec!["DVD_CD"]); } #[test] fn test_parse_page_descriptors() { let data = sample_data(); let sec = parse_security_info(&data, 0x90).unwrap(); assert_eq!(sec.page_descriptor_count, 0x92); // 146 assert_eq!(sec.page_descriptors.len(), 146); // First descriptor: page_count = 0x13 = 19 assert_eq!(sec.page_descriptors[0].page_count, 19); } #[test] fn test_parse_rsa_signature() { let data = sample_data(); let sec = parse_security_info(&data, 0x90).unwrap(); // First bytes of RSA signature from hex dump assert_eq!(sec.rsa_signature[0], 0x2C); assert_eq!(sec.rsa_signature[1], 0x94); assert_eq!(sec.rsa_signature[2], 0xEB); assert_eq!(sec.rsa_signature[3], 0xE6); } #[test] fn test_parse_xgd2_media_id() { let data = sample_data(); let sec = parse_security_info(&data, 0x90).unwrap(); assert_eq!(sec.xgd2_media_id[0], 0x33); assert_eq!(sec.xgd2_media_id[1], 0x51); } #[test] fn test_parse_aes_key() { let data = sample_data(); let sec = parse_security_info(&data, 0x90).unwrap(); assert_eq!(sec.aes_key[0], 0xEA); assert_eq!(sec.aes_key[1], 0xCB); } #[test] fn test_image_flags_display() { let f = ImageFlags(0x00000008); assert_eq!(f.to_string(), "0x00000008 [XGD2_MEDIA_ONLY]"); } #[test] fn test_image_flags_page_size() { assert_eq!(ImageFlags(0x10000000).page_size(), 0x1000); assert_eq!(ImageFlags(0x00000000).page_size(), 0x10000); } #[test] fn test_region_flags_all() { assert_eq!(RegionFlags(0xFFFFFFFF).description(), "ALL REGIONS"); } #[test] fn test_region_flags_ntscu_only() { assert_eq!(RegionFlags(0x000000FF).description(), "NTSC/U"); } #[test] fn test_region_flags_none() { assert_eq!(RegionFlags(0).description(), "NONE"); } #[test] fn test_region_flags_specific() { // NTSC/U + PAL-AU/NZ let r = RegionFlags(0x000100FF); let desc = r.description(); assert!(desc.contains("NTSC/U")); assert!(desc.contains("PAL-AU/NZ")); } #[test] fn test_media_flags_display() { let f = MediaFlags(0x00000004); assert_eq!(f.to_string(), "0x00000004 [DVD_CD]"); } #[test] fn test_media_flags_multiple() { let f = MediaFlags(0x00000003); // HARD_DISK | DVD_X2 let names = f.flag_names(); assert_eq!(names, vec!["HARD_DISK", "DVD_X2"]); } #[test] fn test_section_type_from_raw() { assert_eq!(SectionType::from_raw(1), SectionType::Code); assert_eq!(SectionType::from_raw(2), SectionType::Data); assert_eq!(SectionType::from_raw(3), SectionType::ReadOnlyData); assert!(matches!(SectionType::from_raw(0), SectionType::Unknown(0))); } #[test] fn test_section_type_display() { assert_eq!(SectionType::Code.to_string(), "Code"); assert_eq!(SectionType::Data.to_string(), "Data"); assert_eq!(SectionType::ReadOnlyData.to_string(), "ReadOnly"); assert_eq!(SectionType::Unknown(5).to_string(), "Unknown(5)"); } #[test] fn test_invalid_security_offset() { let data = sample_data(); let err = parse_security_info(&data, 0xFFFFFFFF).unwrap_err(); assert!(matches!(err, Xex2Error::InvalidOffset { .. })); } }