Skip to main content

mediatek_optical/
envelope.rs

1//! Validated input framing, independent of the running receiver's wire format.
2//!
3//! Loading performs no device I/O. A plaintext BIN and its OEM AES transport
4//! representation resolve to the same canonical image. Neither representation
5//! determines how that image must be sent to a different running receiver.
6use sha2::{Digest, Sha256};
7
8use crate::{image, layout, update::PrepareError, Chip};
9
10/// Encoding of the original input bytes, not the required output encoding.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum InputEncoding {
13    /// Canonical stored firmware bytes.
14    Plaintext,
15    /// OEM AES transport representation.
16    Encrypted,
17}
18
19/// Why an input could not be loaded as a unique validated image.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum Error {
22    /// Size, integrity, chip or stored-image checks failed.
23    Image(PrepareError),
24    /// Neither representation has a supported boot banner.
25    UnknownFraming,
26    /// Both representations validated; choosing either would be a guess.
27    Ambiguous,
28}
29impl core::fmt::Display for Error {
30    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
31        match self {
32            Self::Image(error) => write!(f, "{error}"),
33            Self::UnknownFraming => f.write_str("unrecognized MediaTek image framing"),
34            Self::Ambiguous => f.write_str("ambiguous MediaTek image framing"),
35        }
36    }
37}
38impl std::error::Error for Error {
39    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
40        match self {
41            Self::Image(source) => Some(source),
42            _ => None,
43        }
44    }
45}
46
47/// Immutable input and canonical image, validated without authorizing a flash.
48#[derive(Clone)]
49pub struct Envelope {
50    original: Vec<u8>,
51    canonical: Vec<u8>,
52    encoding: InputEncoding,
53    chip: Chip,
54    input_hash: [u8; 32],
55    image_hash: [u8; 32],
56}
57
58impl core::fmt::Debug for Envelope {
59    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
60        f.debug_struct("Envelope")
61            .field("length", &self.canonical.len())
62            .field("input_encoding", &self.encoding)
63            .field("chip", &self.chip)
64            .field("input_sha256", &self.input_hash)
65            .field("image_sha256", &self.image_hash)
66            .finish_non_exhaustive()
67    }
68}
69
70fn recognized(bytes: &[u8]) -> bool {
71    layout::BANNER
72        .of(bytes)
73        .and_then(Chip::from_banner)
74        .is_some()
75}
76
77fn validate(bytes: &[u8]) -> Result<Chip, PrepareError> {
78    if !image::cmac::verify(bytes) {
79        return Err(PrepareError::Integrity);
80    }
81    if image::is_drive_read(bytes) {
82        return Err(PrepareError::DriveRead);
83    }
84    image::detect_chip(bytes)
85        .map(|info| info.family)
86        .map_err(|_| PrepareError::UnknownChip)
87}
88
89impl Envelope {
90    /// Load exactly one 2 MiB plaintext or OEM encrypted image. Positive boot
91    /// framing and a nonempty valid CMAC table are required. Raw captures are
92    /// rejected. No model, filename or absence of encryption selects a format.
93    pub fn load(input: &[u8]) -> Result<Self, Error> {
94        if input.len() != layout::IMAGE_SIZE {
95            return Err(Error::Image(PrepareError::Size(input.len())));
96        }
97        let plain = recognized(input).then(|| validate(input));
98        let mut decoded = input.to_vec();
99        crate::enc::decrypt(&mut decoded).expect("complete image contains whole AES blocks");
100        let encrypted = recognized(&decoded).then(|| validate(&decoded));
101        let (canonical, encoding, chip) = match (plain, encrypted) {
102            (Some(Ok(_)), Some(Ok(_))) => return Err(Error::Ambiguous),
103            (Some(Ok(chip)), _) => (input.to_vec(), InputEncoding::Plaintext, chip),
104            (_, Some(Ok(chip))) => (decoded, InputEncoding::Encrypted, chip),
105            (Some(Err(error)), _) | (_, Some(Err(error))) => return Err(Error::Image(error)),
106            (None, None) => return Err(Error::UnknownFraming),
107        };
108        Ok(Self {
109            original: input.to_vec(),
110            input_hash: Sha256::digest(input).into(),
111            image_hash: Sha256::digest(&canonical).into(),
112            canonical,
113            encoding,
114            chip,
115        })
116    }
117
118    /// Original input, retained unchanged for provenance.
119    pub fn original(&self) -> &[u8] {
120        &self.original
121    }
122    /// Canonical stored image, used for acceptance and readback verification.
123    pub fn image(&self) -> &[u8] {
124        &self.canonical
125    }
126    /// Original input encoding; does not select the receiver's transfer mode.
127    pub fn input_encoding(&self) -> InputEncoding {
128        self.encoding
129    }
130    /// Controller generation established by canonical image analysis.
131    pub fn chip(&self) -> Chip {
132        self.chip
133    }
134    /// SHA-256 of the original bytes.
135    pub fn input_sha256(&self) -> &[u8; 32] {
136        &self.input_hash
137    }
138    /// SHA-256 of canonical stored bytes.
139    pub fn image_sha256(&self) -> &[u8; 32] {
140        &self.image_hash
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::fixtures::{self, BU3};
148
149    #[test]
150    fn both_input_formats_have_one_canonical_identity_and_preserve_provenance() {
151        let image = fixtures::image(&BU3, 7);
152        let plain = Envelope::load(&image).unwrap();
153        let mut wire = image.clone();
154        crate::enc::encrypt(&mut wire).unwrap();
155        let encrypted = Envelope::load(&wire).unwrap();
156        assert_eq!(plain.input_encoding(), InputEncoding::Plaintext);
157        assert_eq!(encrypted.input_encoding(), InputEncoding::Encrypted);
158        assert_eq!(plain.image(), encrypted.image());
159        assert_eq!(plain.image_sha256(), encrypted.image_sha256());
160        assert_ne!(plain.input_sha256(), encrypted.input_sha256());
161        assert_eq!(encrypted.original(), wire);
162        assert_eq!(plain.original(), image);
163    }
164
165    #[test]
166    fn rejects_capture_corruption_empty_integrity_and_unknown_framing() {
167        let image = fixtures::image(&BU3, 3);
168        assert_eq!(
169            Envelope::load(&fixtures::drive_read(&image)).unwrap_err(),
170            Error::Image(PrepareError::DriveRead)
171        );
172        let mut bad = image.clone();
173        bad[fixtures::SIGNED.0 as usize] ^= 1;
174        assert_eq!(
175            Envelope::load(&bad).unwrap_err(),
176            Error::Image(PrepareError::Integrity)
177        );
178        bad = image;
179        let table = image::cmac::TABLE_OFFSET;
180        bad[table..table + image::cmac::ENTRY_COUNT * image::cmac::ENTRY_SIZE].fill(0xff);
181        assert_eq!(
182            Envelope::load(&bad).unwrap_err(),
183            Error::Image(PrepareError::Integrity)
184        );
185        assert_eq!(
186            Envelope::load(&vec![0; layout::IMAGE_SIZE]).unwrap_err(),
187            Error::UnknownFraming
188        );
189        assert_eq!(
190            Envelope::load(&[0; 16]).unwrap_err(),
191            Error::Image(PrepareError::Size(16))
192        );
193    }
194}