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///
21/// Hosts must keep a wildcard arm; the set of variants may grow.
22///
23/// ```compile_fail,E0004
24/// fn f(e: mediatek_optical::envelope::Error) -> u8 { match e { mediatek_optical::envelope::Error::Image { .. } => 0, mediatek_optical::envelope::Error::UnknownFraming { .. } => 1, mediatek_optical::envelope::Error::Ambiguous { .. } => 2, } }
25/// ```
26#[derive(Debug, Clone, PartialEq, Eq)]
27#[non_exhaustive]
28pub enum Error {
29    /// Size, integrity, chip or stored-image checks failed.
30    Image(PrepareError),
31    /// Neither representation has a supported boot banner.
32    UnknownFraming,
33    /// Both representations validated; choosing either would be a guess.
34    Ambiguous,
35}
36impl core::fmt::Display for Error {
37    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
38        match self {
39            Self::Image(error) => write!(f, "{error}"),
40            Self::UnknownFraming => f.write_str("unrecognized MediaTek image framing"),
41            Self::Ambiguous => f.write_str("ambiguous MediaTek image framing"),
42        }
43    }
44}
45impl std::error::Error for Error {
46    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
47        match self {
48            Self::Image(source) => Some(source),
49            Self::UnknownFraming | Self::Ambiguous => None,
50        }
51    }
52}
53
54/// Immutable input and canonical image, validated without authorizing a flash.
55#[derive(Clone)]
56pub struct Envelope {
57    original: Vec<u8>,
58    /// `None` when the canonical image is the original bytes (plaintext input).
59    canonical: Option<Vec<u8>>,
60    encoding: InputEncoding,
61    chip: Chip,
62    input_hash: [u8; 32],
63    image_hash: [u8; 32],
64}
65
66impl core::fmt::Debug for Envelope {
67    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
68        f.debug_struct("Envelope")
69            .field("length", &self.image().len())
70            .field("input_encoding", &self.encoding)
71            .field("chip", &self.chip)
72            .field("input_sha256", &self.input_hash)
73            .field("image_sha256", &self.image_hash)
74            .finish_non_exhaustive()
75    }
76}
77
78fn recognized(bytes: &[u8]) -> bool {
79    layout::BANNER
80        .of(bytes)
81        .and_then(Chip::from_banner)
82        .is_some()
83}
84
85fn validate(bytes: &[u8]) -> Result<Chip, PrepareError> {
86    if !image::cmac::verify(bytes) {
87        return Err(PrepareError::Integrity);
88    }
89    if image::is_drive_read(bytes) {
90        return Err(PrepareError::DriveRead);
91    }
92    image::detect_chip(bytes)
93        .map(|info| info.family)
94        .map_err(|_| PrepareError::UnknownChip)
95}
96
97impl Envelope {
98    /// Load exactly one 2 MiB plaintext or OEM encrypted image. Positive boot
99    /// framing and a nonempty valid CMAC table are required. Raw captures are
100    /// rejected. No model, filename or absence of encryption selects a format.
101    pub fn load(input: &[u8]) -> Result<Self, Error> {
102        if input.len() != layout::IMAGE_SIZE {
103            return Err(Error::Image(PrepareError::Size(input.len())));
104        }
105        let plain = recognized(input).then(|| validate(input));
106        // AES-ECB: a decrypted block depends only on its own ciphertext block,
107        // so decrypting the banner blocks alone decides `recognized(decoded)`.
108        const _: () = assert!(layout::BANNER.start % crate::enc::BLOCK == 0);
109        const _: () = assert!(layout::BANNER.len % crate::enc::BLOCK == 0);
110        let mut banner = [0u8; layout::BANNER.len];
111        banner.copy_from_slice(
112            &input[layout::BANNER.start..layout::BANNER.start + layout::BANNER.len],
113        );
114        crate::enc::decrypt(&mut banner).expect("banner contains whole AES blocks");
115        let decoded = Chip::from_banner(&banner).is_some().then(|| {
116            let mut decoded = input.to_vec();
117            crate::enc::decrypt(&mut decoded).expect("complete image contains whole AES blocks");
118            decoded
119        });
120        let encrypted = decoded.as_deref().map(validate);
121        let (canonical, encoding, chip) = match (plain, encrypted) {
122            (Some(Ok(_)), Some(Ok(_))) => return Err(Error::Ambiguous),
123            (Some(Ok(chip)), _) => (None, InputEncoding::Plaintext, chip),
124            (_, Some(Ok(chip))) => (decoded, InputEncoding::Encrypted, chip),
125            (Some(Err(error)), _) | (_, Some(Err(error))) => return Err(Error::Image(error)),
126            (None, None) => return Err(Error::UnknownFraming),
127        };
128        let input_hash: [u8; 32] = Sha256::digest(input).into();
129        let image_hash = match &canonical {
130            None => input_hash,
131            Some(canonical) => Sha256::digest(canonical).into(),
132        };
133        Ok(Self {
134            original: input.to_vec(),
135            input_hash,
136            image_hash,
137            canonical,
138            encoding,
139            chip,
140        })
141    }
142
143    /// Original input, retained unchanged for provenance.
144    pub fn original(&self) -> &[u8] {
145        &self.original
146    }
147    /// Canonical stored image, used for acceptance and readback verification.
148    pub fn image(&self) -> &[u8] {
149        self.canonical.as_deref().unwrap_or(&self.original)
150    }
151    /// Original input encoding; does not select the receiver's transfer mode.
152    pub fn input_encoding(&self) -> InputEncoding {
153        self.encoding
154    }
155    /// Controller generation established by canonical image analysis.
156    pub fn chip(&self) -> Chip {
157        self.chip
158    }
159    /// SHA-256 of the original bytes.
160    pub fn input_sha256(&self) -> &[u8; 32] {
161        &self.input_hash
162    }
163    /// SHA-256 of canonical stored bytes.
164    pub fn image_sha256(&self) -> &[u8; 32] {
165        &self.image_hash
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use crate::fixtures::{self, BU3};
173
174    #[test]
175    fn both_input_formats_have_one_canonical_identity_and_preserve_provenance() {
176        let image = fixtures::image(&BU3, 7);
177        let plain = Envelope::load(&image).unwrap();
178        let mut wire = image.clone();
179        crate::enc::encrypt(&mut wire).unwrap();
180        let encrypted = Envelope::load(&wire).unwrap();
181        assert_eq!(plain.input_encoding(), InputEncoding::Plaintext);
182        assert_eq!(encrypted.input_encoding(), InputEncoding::Encrypted);
183        assert_eq!(plain.image(), encrypted.image());
184        assert_eq!(plain.image_sha256(), encrypted.image_sha256());
185        assert_ne!(plain.input_sha256(), encrypted.input_sha256());
186        assert_eq!(encrypted.original(), wire);
187        assert_eq!(plain.original(), image);
188    }
189
190    #[test]
191    fn rejects_capture_corruption_empty_integrity_and_unknown_framing() {
192        let image = fixtures::image(&BU3, 3);
193        assert_eq!(
194            Envelope::load(&fixtures::drive_read(&image)).unwrap_err(),
195            Error::Image(PrepareError::DriveRead)
196        );
197        let mut bad = image.clone();
198        bad[fixtures::SIGNED.0 as usize] ^= 1;
199        assert_eq!(
200            Envelope::load(&bad).unwrap_err(),
201            Error::Image(PrepareError::Integrity)
202        );
203        bad = image;
204        let table = image::cmac::TABLE_OFFSET;
205        bad[table..table + image::cmac::ENTRY_COUNT * image::cmac::ENTRY_SIZE].fill(0xff);
206        assert_eq!(
207            Envelope::load(&bad).unwrap_err(),
208            Error::Image(PrepareError::Integrity)
209        );
210        assert_eq!(
211            Envelope::load(&vec![0; layout::IMAGE_SIZE]).unwrap_err(),
212            Error::UnknownFraming
213        );
214        assert_eq!(
215            Envelope::load(&[0; 16]).unwrap_err(),
216            Error::Image(PrepareError::Size(16))
217        );
218    }
219}