use senbei_crypto::android::gf32_mul_fixed; use super::error::{Error, Result, invalid}; pub(crate) const RECORD_SIZE: usize = 0x5c; pub(crate) const DIRECT_FLAG: u32 = 2; #[derive(Debug, Clone, Copy)] pub(crate) struct Record { pub index: usize, pub command_id: u32, pub flags: u32, pub image_offset: u32, pub image_size: u32, pub metadata_offset: u32, pub metadata_size: u32, pub id_copy: u32, pub entry_offset: u32, pub init_offset: u32, } impl Record { pub(crate) fn direct(self) -> bool { self.flags & DIRECT_FLAG != 0 } } #[derive(Debug, Clone, Copy)] pub(crate) struct StreamHeader { pub encrypted_words: [u32; 2], pub decrypted_words: [u32; 2], pub record_state: u32, } pub(crate) fn parse_record_stream( stream: &[u8], stream_id: u32, ) -> Result<(StreamHeader, Vec, usize)> { if stream.len() < 8 { return invalid(format!( "stream 0x{stream_id:02X} is shorter than its 8-byte header" )); } let cipher0 = read_u32(stream, 0)?; let cipher1 = read_u32(stream, 4)?; let key = stream_id.wrapping_mul(0x9d32_3cd7); let shift = stream_id & 7; let base = (key >> shift) .wrapping_add(0x5e72_7d74) .wrapping_add(key.wrapping_shl(stream_id & 0xb)) .wrapping_add(0xf71e_3005); let plain0 = gf32_mul_fixed(cipher0.wrapping_add(0xcbf0_c1d8)) ^ 0xeb_e81dba_u32.wrapping_add(base); let plain1 = gf32_mul_fixed(cipher1.wrapping_add(cipher0)) ^ 0xeb_e81dba_u32.wrapping_mul(5).wrapping_add(base); let header = StreamHeader { encrypted_words: [cipher0, cipher1], decrypted_words: [plain0, plain1], record_state: plain1.wrapping_add(base), }; let mut records = Vec::new(); let mut first_payload = stream.len(); for index in 0..256_usize { let start = 8_usize .checked_add(index.checked_mul(RECORD_SIZE).ok_or_else(|| { Error::Invalid("record descriptor offset overflow".to_owned()) })?) .ok_or_else(|| Error::Invalid("record descriptor offset overflow".to_owned()))?; let end = start .checked_add(RECORD_SIZE) .ok_or_else(|| Error::Invalid("record descriptor end overflow".to_owned()))?; if end > stream.len() { return invalid(format!( "stream 0x{stream_id:02X} descriptor table is truncated at record {index}" )); } let record = decrypt_record(&stream[start..end], index, header.record_state)?; if record.id_copy != 0 && record.command_id != record.id_copy { return invalid(format!( "stream 0x{stream_id:02X} record {index} command/id mismatch: 0x{:X} != 0x{:X}", record.command_id, record.id_copy )); } for (offset, size) in [ (record.image_offset, record.image_size), (record.metadata_offset, record.metadata_size), ] { if offset != 0 && size != 0 { let offset = usize::try_from(offset).map_err(|_| { Error::Invalid(format!( "stream 0x{stream_id:02X} record {index} payload offset exceeds usize" )) })?; if offset >= stream.len() { return invalid(format!( "stream 0x{stream_id:02X} record {index} payload offset 0x{offset:x} exceeds stream 0x{:x}", stream.len() )); } first_payload = first_payload.min(offset); } } records.push(record); if end == first_payload { return Ok((header, records, first_payload)); } if end > first_payload { return invalid(format!( "stream 0x{stream_id:02X} descriptor table crosses first payload at 0x{first_payload:x}" )); } } invalid(format!( "stream 0x{stream_id:02X} has no descriptor boundary in 256 records" )) } fn decrypt_record(raw: &[u8], index: usize, state: u32) -> Result { if raw.len() != RECORD_SIZE { return invalid(format!( "record {index} has size 0x{:x}, expected 0x{RECORD_SIZE:x}", raw.len() )); } let product = state.wrapping_add(0x96f6_0b71).wrapping_mul(state); let index_mask = product.wrapping_shl(((index + 1) & 3) as u32); let mix = state.wrapping_mul(0x06a5_5bcc).wrapping_add(product); let mut accumulator = 0x7993_4cf6_u32; let mut feedback = 0xf02f_7685_u32; let mut words = [0_u32; RECORD_SIZE / 4]; for (word_index, chunk) in raw.chunks_exact(4).enumerate() { feedback = feedback.wrapping_mul(feedback); let cipher = u32::from_le_bytes( chunk .try_into() .map_err(|_| Error::Invalid("record word has an invalid size".to_owned()))?, ); let mut value = gf32_mul_fixed(cipher ^ (feedback >> 3)) ^ index_mask; value = value.wrapping_add(accumulator).wrapping_add(state); value = value.wrapping_sub(mix >> ((word_index * 4 + 3) & 5)); words[word_index] = value; accumulator = accumulator.wrapping_add(0xe64d_33d8); feedback = cipher; } Ok(Record { index, command_id: words[0], flags: words[1], image_offset: words[2], image_size: words[3], metadata_offset: words[4], metadata_size: words[5], id_copy: words[6], entry_offset: words[7], init_offset: words[8], }) } fn read_u32(data: &[u8], offset: usize) -> Result { let bytes = data.get(offset..offset + 4).ok_or_else(|| { Error::Invalid(format!("record header range 0x{offset:x} is out of bounds")) })?; Ok(u32::from_le_bytes(bytes.try_into().map_err(|_| { Error::Invalid("invalid record u32 range".to_owned()) })?)) }