Skip to main content

mediatek_optical/
receiver.rs

1//! Read-only detection of the active MediaTek update receiver.
2//!
3//! BOOT and application receivers can require different transfer encodings,
4//! even in the same image. Positive Thumb code signatures identify the direct
5//! banner comparison or the decrypt-before-banner-check path. Neither a model,
6//! revision date, target image nor absence of AES selects plaintext.
7//!
8//! The bounded search covers the receiver locations in the audited MT1939 /
9//! MT1959 corpus. Unreadable or unrecognized code returns an error. This is not
10//! permission to flash and does not replace image/hardware compatibility gates.
11use crate::{
12    cdb,
13    drive::{self, Transport},
14    INQUIRY_LEN,
15};
16
17pub mod entry;
18
19#[cfg(all(feature = "image", feature = "enc"))]
20pub mod analysis;
21#[cfg(all(feature = "image", feature = "enc"))]
22pub mod decoding;
23#[cfg(all(feature = "image", feature = "enc"))]
24pub mod layout;
25#[cfg(all(feature = "image", feature = "enc"))]
26pub mod programming;
27
28#[cfg(all(feature = "image", feature = "enc"))]
29mod mixed;
30#[cfg(all(feature = "image", feature = "enc"))]
31mod prepared;
32#[cfg(all(feature = "image", feature = "enc"))]
33pub use prepared::{ExecuteError, PreparedUpdate, TransferEncoding, TransferPlan};
34#[cfg(all(feature = "image", feature = "enc"))]
35mod profile;
36#[cfg(all(feature = "image", feature = "enc"))]
37pub mod reference;
38#[cfg(all(feature = "image", feature = "enc"))]
39pub use profile::detect;
40
41/// Code currently handling commands.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum Mode {
44    /// Recovery bootloader (INQUIRY revision `BOOT`).
45    Boot,
46    /// Running firmware application.
47    Firmware,
48}
49
50/// Encoding of a region in a receiver transfer plan.
51/// Legacy receiver detection reports the header path; executable plans expose
52/// header and body separately through `TransferEncoding`.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum Encoding {
55    /// Send the canonical image unchanged.
56    Plaintext,
57    /// Send the AES-128-ECB transport envelope.
58    Encrypted,
59}
60
61/// A live INQUIRY binding, obtained by [`detect_mode`].
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct DetectedMode {
64    mode: Mode,
65    inquiry: [u8; INQUIRY_LEN],
66}
67impl DetectedMode {
68    /// Whether BOOT or the application is running.
69    pub fn mode(&self) -> Mode {
70        self.mode
71    }
72    /// Standard INQUIRY bytes used to bind this detection.
73    pub fn inquiry(&self) -> &[u8; INQUIRY_LEN] {
74        &self.inquiry
75    }
76}
77
78/// Detection failed; no implicit encoding is selected.
79#[derive(Debug)]
80pub enum Error<E> {
81    /// A required read failed or was short.
82    Read(drive::Error<E>),
83    /// INQUIRY is not a complete optical-drive identity with a revision.
84    InvalidIdentity,
85    /// No positive receiver signature was readable in the supported range.
86    Unknown,
87    /// More than one receiver location or encoding matched.
88    Conflict,
89    /// Identity or receiver bytes changed after detection.
90    Changed,
91    /// The running receiver's entry grammar is unreadable or unrecognized.
92    UnknownEntry,
93}
94impl<E: core::fmt::Debug> core::fmt::Display for Error<E> {
95    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
96        match self {
97            Self::Read(e) => write!(f, "receiver read failed: {e}"),
98            Self::InvalidIdentity => f.write_str("invalid MediaTek receiver INQUIRY identity"),
99            Self::Unknown => f.write_str(
100                "active MediaTek receiver signature unreadable or unknown; encoding not selected",
101            ),
102            Self::Conflict => {
103                f.write_str("conflicting MediaTek receiver signatures; encoding not selected")
104            }
105            Self::Changed => {
106                f.write_str("MediaTek receiver changed since detection; re-plan the flash")
107            }
108            Self::UnknownEntry => f.write_str(
109                "active MediaTek entry contract unreadable or unknown; no PREPARE selected",
110            ),
111        }
112    }
113}
114#[cfg(feature = "std")]
115impl<E: core::fmt::Debug + 'static> std::error::Error for Error<E> {
116    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
117        match self {
118            Self::Read(source) => Some(source),
119            _ => None,
120        }
121    }
122}
123
124/// Identify BOOT versus running firmware using standard INQUIRY only.
125/// No feature 0x010C, descriptor, update command or data-out is required.
126pub fn detect_mode<T: Transport>(t: &mut T) -> Result<DetectedMode, Error<T::Error>> {
127    let mut inquiry = [0; INQUIRY_LEN];
128    drive::read(
129        t,
130        &cdb::inquiry(INQUIRY_LEN as u16),
131        &mut inquiry,
132        INQUIRY_LEN,
133    )
134    .map_err(Error::Read)?;
135    let revision = &inquiry[32..36];
136    if inquiry[0] & 0x1f != 5
137        || inquiry[4] < 31
138        || !revision.iter().all(|b| b.is_ascii_graphic() || *b == b' ')
139        || revision.iter().all(|b| *b == b' ')
140    {
141        return Err(Error::InvalidIdentity);
142    }
143    let mode = if revision == b"BOOT" {
144        Mode::Boot
145    } else {
146        Mode::Firmware
147    };
148    Ok(DetectedMode { mode, inquiry })
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
152struct Evidence {
153    address: u32,
154    len: usize,
155    bytes: [u8; 64],
156}
157impl Evidence {
158    fn new(address: u32, bytes: &[u8]) -> Self {
159        let mut e = Self {
160            address,
161            len: bytes.len(),
162            bytes: [0; 64],
163        };
164        e.bytes[..bytes.len()].copy_from_slice(bytes);
165        e
166    }
167}
168
169/// Positive receiver evidence, bound to the live mode and code bytes.
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct Receiver {
172    identity: DetectedMode,
173    encoding: Encoding,
174    site: Evidence,
175    transform: Option<Evidence>,
176    #[cfg(all(feature = "image", feature = "enc"))]
177    profile: Option<profile::Profile>,
178    #[cfg(all(feature = "image", feature = "enc"))]
179    reference: Option<reference::Binding>,
180}
181impl Receiver {
182    /// Cached application evidence from the same snapshot used by live entry
183    /// detection. This is analysis, not a completed transfer authorization.
184    #[cfg(all(feature = "image", feature = "enc"))]
185    pub fn application_analysis(&self) -> Option<&analysis::ApplicationAnalysis> {
186        self.profile.as_ref()?.application.as_ref()
187    }
188    /// Active receiver mode.
189    pub fn mode(&self) -> Mode {
190        self.identity.mode
191    }
192    /// Header-path classification retained for compatibility. Consult the
193    /// prepared transfer plan for separate header/body wire encodings.
194    pub fn encoding(&self) -> Encoding {
195        self.encoding
196    }
197    /// PREPARE dialect from full live entry detection; encoding-only detection
198    /// deliberately supplies no command.
199    #[cfg(all(feature = "image", feature = "enc"))]
200    pub fn prepare_command(&self) -> Option<cdb::Prepare> {
201        self.profile.as_ref().map(|profile| profile.prepare)
202    }
203    /// Address of the matched receiver instructions.
204    pub fn signature_address(&self) -> u32 {
205        self.site.address
206    }
207    /// Re-read identity and matched instructions immediately before writing.
208    pub fn revalidate<T: Transport>(&self, t: &mut T) -> Result<(), Error<T::Error>> {
209        #[cfg(all(feature = "image", feature = "enc"))]
210        if let Some(reference) = &self.reference {
211            return reference.revalidate(t);
212        }
213        self.revalidate_signature(t)?;
214        #[cfg(all(feature = "image", feature = "enc"))]
215        if let Some(profile) = &self.profile {
216            profile.revalidate(t)?;
217        }
218        Ok(())
219    }
220
221    // Detection finishes with a small coherence check. The complete snapshot
222    // is checked separately, immediately before any mutating execution.
223    fn revalidate_signature<T: Transport>(&self, t: &mut T) -> Result<(), Error<T::Error>> {
224        if detect_mode(t)? != self.identity {
225            return Err(Error::Changed);
226        }
227        for e in core::iter::once(&self.site).chain(self.transform.iter()) {
228            let mut bytes = [0; 64];
229            drive::read_memory(t, e.address, &mut bytes[..e.len]).map_err(Error::Read)?;
230            if bytes[..e.len] != e.bytes[..e.len] {
231                return Err(Error::Changed);
232            }
233        }
234        Ok(())
235    }
236}
237
238// 0x100 masks a relocation/stack-slot byte. These are positive instruction
239// sequences, not whole-build hashes. Matches are halfword aligned.
240const ANY: u16 = 0x100;
241const PLAIN_DIRECT: &[u16] = &[
242    0x03, 0x21, 0x09, 0x03, 0x61, 0x18, ANY, 0xa8, 0x00, 0x5d, 0x09, 0x78, 0x88, 0x42,
243];
244const PLAIN_APP: &[u16] = &[
245    0x03, 0x20, ANY, 0x49, 0x00, 0x03, 0x20, 0x18, 0x09, 0x68, 0x00, 0x02, 0x00, 0x0a, 0x40, 0x18,
246    ANY, 0xa9, 0x09, 0x5d, 0x00, 0x78, 0x88, 0x42,
247];
248const PLAIN_BOOT: &[u16] = &[
249    0x03, 0x20, 0x00, 0x03, 0x20, 0x18, 0xf9, 0x69, 0x00, 0x02, 0x00, 0x0a, 0x40, 0x18, ANY, 0xa9,
250    0x09, 0x5d, 0x00, 0x78, 0x88, 0x42,
251];
252const PLAIN_LOGGED: &[u16] = &[
253    0x03, 0x20, 0x00, 0x03, 0x20, 0x18, 0x05, 0x02, 0xf8, 0x69, 0x2d, 0x0a, 0x28, 0x18, 0x02, 0x78,
254    ANY, 0x48, 0x21, 0x00, ANY, ANY, ANY, ANY, 0xf8, 0x69, 0x28, 0x18, 0x00, 0x78, 0x31, 0x5d,
255    0x88, 0x42,
256];
257const AES: &[u16] = &[
258    0xf7, 0xb5, 0x06, 0x00, 0x14, 0x00, ANY, 0x48, 0x11, 0x00, 0x84, 0xb0, ANY, ANY, ANY, ANY,
259    0x00, 0x2c, 0x01, 0xd0, 0x20, 0x07, 0x02, 0xd0, 0x00, 0x20, 0x07, 0xb0, 0xf0, 0xbd, 0xa0, 0x08,
260    0x01, 0x22, 0x92, 0x06, 0x02, 0x90, 0x10, 0x68, 0x10, 0x21, 0x88, 0x43, 0x10, 0x60,
261];
262fn matches(bytes: &[u8], pattern: &[u16]) -> bool {
263    bytes.len() >= pattern.len()
264        && bytes
265            .iter()
266            .zip(pattern)
267            .all(|(&b, &p)| p == ANY || p == b as u16)
268}
269fn contains(bytes: &[u8], needle: &[u8]) -> bool {
270    bytes.windows(needle.len()).any(|b| b == needle)
271}
272fn branch_target(bytes: &[u8], address: u32) -> Option<u32> {
273    let a = u16::from_le_bytes([bytes[0], bytes[1]]);
274    let b = u16::from_le_bytes([bytes[2], bytes[3]]);
275    if a & 0xf800 != 0xf000 || b & 0xf800 != 0xf800 {
276        return None;
277    }
278    let delta = (((a & 2047) as i32) << 12) | (((b & 2047) as i32) << 1);
279    let delta = (delta << 9) >> 9;
280    u32::try_from(address as i64 + 4 + delta as i64).ok()
281}
282
283/// Detect the transfer encoding by reading the active receiver's instructions.
284/// Reads memory only; never stages data or tries alternative write formats.
285/// `mode` must come from [`detect_mode`]; identity is checked again on return.
286pub fn detect_encoding<T: Transport>(
287    t: &mut T,
288    mode: &DetectedMode,
289) -> Result<Receiver, Error<T::Error>> {
290    let (start, end) = match mode.mode {
291        Mode::Boot => (0x3400, 0x3800),
292        Mode::Firmware => (0x98000, 0xb4000),
293    };
294    let mut found: Option<Receiver> = None;
295    // 64-byte overlap preserves instructions spanning a read boundary.
296    let mut block = [0; 1088];
297    let mut base = start;
298    while base < end {
299        let len = core::cmp::min(block.len(), (end - base) as usize);
300        if drive::read_memory(t, base, &mut block[..len]).is_ok() {
301            for off in (0..len.saturating_sub(4)).step_by(2) {
302                let bytes = &block[off..len];
303                let address = base + off as u32;
304                let mut candidate = None;
305                for pattern in [PLAIN_DIRECT, PLAIN_APP, PLAIN_BOOT, PLAIN_LOGGED] {
306                    if matches(bytes, pattern) {
307                        candidate = Some(Receiver {
308                            identity: mode.clone(),
309                            encoding: Encoding::Plaintext,
310                            site: Evidence::new(address, &bytes[..pattern.len()]),
311                            transform: None,
312                            #[cfg(all(feature = "image", feature = "enc"))]
313                            profile: None,
314                            #[cfg(all(feature = "image", feature = "enc"))]
315                            reference: None,
316                        });
317                    }
318                }
319                // Validate a header-decrypt call AND its hardware AES routine.
320                // The caller must pass staged header +0x3000 and length 16.
321                let before = &block[off.saturating_sub(32)..off];
322                if off >= 32
323                    && contains(before, &[0x10, 0x22])
324                    && (contains(before, &[3, 0x20, 0, 3])
325                        || contains(before, &[3, 0x23]) && contains(before, &[0x1b, 3]))
326                {
327                    if let Some(target) = branch_target(bytes, address) {
328                        let valid_range = match mode.mode {
329                            Mode::Boot => target < 0x10000,
330                            Mode::Firmware => (0x10000..0x1c0000).contains(&target),
331                        };
332                        let mut routine = [0; 46];
333                        if valid_range
334                            && drive::read_memory(t, target, &mut routine).is_ok()
335                            && matches(&routine, AES)
336                        {
337                            let begin = off.saturating_sub(32);
338                            candidate = Some(Receiver {
339                                identity: mode.clone(),
340                                encoding: Encoding::Encrypted,
341                                site: Evidence::new(base + begin as u32, &block[begin..off + 4]),
342                                transform: Some(Evidence::new(target, &routine)),
343                                #[cfg(all(feature = "image", feature = "enc"))]
344                                profile: None,
345                                #[cfg(all(feature = "image", feature = "enc"))]
346                                reference: None,
347                            });
348                        }
349                    }
350                }
351                if let Some(candidate) = candidate {
352                    if let Some(previous) = &found {
353                        if previous != &candidate {
354                            return Err(Error::Conflict);
355                        }
356                    } else {
357                        found = Some(candidate);
358                    }
359                }
360            }
361        }
362        base += 1024;
363    }
364    let receiver = found.ok_or(Error::Unknown)?;
365    receiver.revalidate(t)?;
366    Ok(receiver)
367}
368
369#[cfg(test)]
370mod tests;