Skip to main content

mediatek_optical/receiver/
layout.rs

1//! Solve wire encoding from ordered receiver operations, not an AES signature.
2//!
3//! This is an offline proof component. Operations must come from a separately
4//! validated, complete receiver path; a layout alone never authorizes I/O.
5//! Scratch header decoding and in-place body decoding have different effects.
6use core::ops::Range;
7
8const BLOCK: usize = 16;
9
10/// An operation whose inputs, range and order the receiver analyzer established.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum Operation {
13    /// Decode to temporary storage and compare the result with plaintext.
14    /// Staging memory remains unchanged.
15    CheckDecrypted(Range<usize>),
16    /// Decode staged blocks in place with the transport AES key.
17    DecryptInPlace(Range<usize>),
18    /// A signature, descriptor or integrity check consumes plaintext staging.
19    CheckPlaintext(Range<usize>),
20    /// These staged bytes become the installed image and must be plaintext.
21    Program(Range<usize>),
22}
23
24/// Contradictory or unsupported receiver operations cannot supply a wire plan.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum Error {
27    /// Canonical images and AES transforms require complete blocks.
28    Alignment,
29    /// An operation escapes the supplied image or has an empty/reversed range.
30    Range,
31    /// The same block is required in both ciphertext and plaintext form.
32    Conflict {
33        /// Byte offset of the conflicting AES block.
34        block: usize,
35    },
36    /// The receiver would decrypt the same block more than once.
37    MultipleDecryptions {
38        /// Byte offset of the block requiring unsupported nested encoding.
39        block: usize,
40    },
41    /// No program operation was supplied, so there is no installed-image proof.
42    NoProgram,
43    /// Encoding was requested for a different-sized image.
44    ImageSize,
45}
46
47impl core::fmt::Display for Error {
48    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
49        match self {
50            Self::Alignment => f.write_str("receiver encoding requires complete AES blocks"),
51            Self::Range => f.write_str("receiver operation has an empty or out-of-bounds range"),
52            Self::Conflict { block } => write!(f, "conflicting wire encodings at {block:#x}"),
53            Self::MultipleDecryptions { block } => write!(f, "repeated AES decoding at {block:#x}"),
54            Self::NoProgram => f.write_str("receiver layout contains no programming operation"),
55            Self::ImageSize => f.write_str("image size differs from the receiver layout"),
56        }
57    }
58}
59
60impl std::error::Error for Error {}
61
62/// Encoding ranges derived from constraints on every observed/programmed block.
63/// Unconsumed bytes stay canonical. This does not attest to parser completeness.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct Layout {
66    size: usize,
67    encrypted: Vec<Range<usize>>,
68    programmed: Vec<Range<usize>>,
69}
70
71impl Layout {
72    /// Contiguous AES-aligned ranges to encrypt once in the outgoing image.
73    pub fn encrypted_ranges(&self) -> &[Range<usize>] {
74        &self.encrypted
75    }
76
77    /// Ranges for which the operation sequence establishes plaintext at write.
78    pub fn programmed_ranges(&self) -> &[Range<usize>] {
79        &self.programmed
80    }
81
82    /// Create wire bytes without changing the canonical image.
83    /// The caller still needs a complete, bound receiver execution contract.
84    pub fn encode(&self, image: &[u8]) -> Result<Vec<u8>, Error> {
85        if image.len() != self.size {
86            return Err(Error::ImageSize);
87        }
88        let mut wire = image.to_vec();
89        for range in &self.encrypted {
90            crate::enc::encrypt(&mut wire[range.clone()]).map_err(|_| Error::Alignment)?;
91        }
92        Ok(wire)
93    }
94}
95
96fn blocks(range: &Range<usize>, size: usize, aligned: bool) -> Result<Range<usize>, Error> {
97    if range.start >= range.end || range.end > size {
98        return Err(Error::Range);
99    }
100    if aligned && (range.start % BLOCK != 0 || range.end % BLOCK != 0) {
101        return Err(Error::Alignment);
102    }
103    Ok(range.start / BLOCK..range.end.div_ceil(BLOCK))
104}
105
106/// Derive the initial encoding required by the ordered checks and writes.
107/// A scratch decode adds a requirement without altering staging state; an
108/// in-place decode changes the requirements of all subsequent consumers.
109pub fn analyze(size: usize, operations: &[Operation]) -> Result<Layout, Error> {
110    if size == 0 || size % BLOCK != 0 {
111        return Err(Error::Alignment);
112    }
113    let mut decoded = vec![false; size / BLOCK];
114    let mut required = vec![None; size / BLOCK];
115    let mut programmed = Vec::new();
116    for operation in operations {
117        let (range, aligned) = match operation {
118            Operation::CheckDecrypted(r) | Operation::DecryptInPlace(r) => (r, true),
119            Operation::CheckPlaintext(r) | Operation::Program(r) => (r, false),
120        };
121        for block in blocks(range, size, aligned)? {
122            if matches!(operation, Operation::DecryptInPlace(_)) {
123                if decoded[block] {
124                    return Err(Error::MultipleDecryptions {
125                        block: block * BLOCK,
126                    });
127                }
128                decoded[block] = true;
129                continue;
130            }
131            let scratch = matches!(operation, Operation::CheckDecrypted(_));
132            if scratch && decoded[block] {
133                return Err(Error::MultipleDecryptions {
134                    block: block * BLOCK,
135                });
136            }
137            let encrypted = scratch || decoded[block];
138            if required[block].is_some_and(|previous| previous != encrypted) {
139                return Err(Error::Conflict {
140                    block: block * BLOCK,
141                });
142            }
143            required[block] = Some(encrypted);
144        }
145        if let Operation::Program(range) = operation {
146            programmed.push(range.clone());
147        }
148    }
149    if programmed.is_empty() {
150        return Err(Error::NoProgram);
151    }
152    let mut encrypted: Vec<Range<usize>> = Vec::new();
153    for (block, value) in required.into_iter().enumerate() {
154        if value != Some(true) {
155            continue;
156        }
157        let start = block * BLOCK;
158        match encrypted.last_mut() {
159            Some(previous) if previous.end == start => previous.end += BLOCK,
160            _ => encrypted.push(start..start + BLOCK),
161        }
162    }
163    Ok(Layout {
164        size,
165        encrypted,
166        programmed,
167    })
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    const SIZE: usize = 0x20_0000;
175    const HEADER: Range<usize> = 0x3000..0x3010;
176
177    #[test]
178    fn scratch_decode_does_not_make_programmed_header_plaintext() {
179        let operations = [
180            Operation::CheckDecrypted(HEADER),
181            Operation::Program(0..SIZE),
182        ];
183        assert_eq!(
184            analyze(SIZE, &operations),
185            Err(Error::Conflict { block: 0x3000 })
186        );
187    }
188
189    #[test]
190    fn boot_integrity_check_also_prevents_header_only_encoding() {
191        let operations = [
192            Operation::CheckDecrypted(HEADER),
193            Operation::CheckPlaintext(0x1000..0xfff0),
194            Operation::Program(0x10000..SIZE),
195        ];
196        assert_eq!(
197            analyze(SIZE, &operations),
198            Err(Error::Conflict { block: 0x3000 })
199        );
200    }
201
202    #[test]
203    fn preserved_unchecked_boot_allows_header_only_encoding() {
204        let operations = [
205            Operation::CheckDecrypted(HEADER),
206            Operation::CheckPlaintext(0x10600..0x10608),
207            Operation::Program(0x10000..SIZE),
208        ];
209        let plan = analyze(SIZE, &operations).unwrap();
210        assert_eq!(plan.encrypted_ranges(), &[HEADER]);
211        let image = vec![0x5a; SIZE];
212        let wire = plan.encode(&image).unwrap();
213        assert_eq!(wire[..HEADER.start], image[..HEADER.start]);
214        assert_ne!(wire[HEADER.clone()], image[HEADER.clone()]);
215        assert_eq!(wire[HEADER.end..], image[HEADER.end..]);
216        let mut header = wire[HEADER.clone()].to_vec();
217        crate::enc::decrypt(&mut header).unwrap();
218        assert_eq!(header, image[HEADER]);
219    }
220
221    #[test]
222    fn full_decode_before_checks_and_programming_requires_full_encryption() {
223        let operations = [
224            Operation::CheckDecrypted(HEADER),
225            Operation::DecryptInPlace(0..SIZE),
226            Operation::CheckPlaintext(0x10600..0x10608),
227            Operation::Program(0..SIZE),
228        ];
229        let plan = analyze(SIZE, &operations).unwrap();
230        assert_eq!(plan.encrypted_ranges().len(), 1);
231        assert_eq!(plan.encrypted_ranges()[0], 0..SIZE);
232        let mut reordered = operations.to_vec();
233        reordered.swap(1, 2);
234        assert_eq!(
235            analyze(SIZE, &reordered),
236            Err(Error::Conflict { block: 0x10600 })
237        );
238    }
239
240    #[test]
241    fn plaintext_and_body_only_are_distinct_constraints() {
242        let plain = analyze(SIZE, &[Operation::Program(0..SIZE)]).unwrap();
243        assert!(plain.encrypted_ranges().is_empty());
244        let body = analyze(
245            SIZE,
246            &[
247                Operation::CheckPlaintext(HEADER),
248                Operation::DecryptInPlace(0x10000..SIZE),
249                Operation::Program(0x10000..SIZE),
250            ],
251        )
252        .unwrap();
253        assert_eq!(body.encrypted_ranges().len(), 1);
254        assert_eq!(body.encrypted_ranges()[0], 0x10000..SIZE);
255    }
256
257    #[test]
258    fn reject_unbounded_unaligned_repeated_and_incomplete_operations() {
259        assert_eq!(
260            analyze(SIZE, &[Operation::Program(0..SIZE + 1)]),
261            Err(Error::Range)
262        );
263        assert_eq!(
264            analyze(SIZE, &[Operation::CheckDecrypted(1..17)]),
265            Err(Error::Alignment)
266        );
267        assert_eq!(
268            analyze(SIZE, &[Operation::CheckPlaintext(HEADER)]),
269            Err(Error::NoProgram)
270        );
271        assert_eq!(
272            analyze(
273                SIZE,
274                &[
275                    Operation::DecryptInPlace(HEADER),
276                    Operation::CheckDecrypted(HEADER),
277                ]
278            ),
279            Err(Error::MultipleDecryptions { block: 0x3000 })
280        );
281    }
282}