Skip to main content

mediatek_optical/image/
cmac.rs

1//! The AES-CMAC integrity table.
2//!
3//! Every MT1959 and MT1939 image carries a 16-entry integrity table at file
4//! offset `0x10400` ([`crate::layout::CMAC_TABLE`]). Each entry is 28 bytes, little-endian:
5//!
6//! ```text
7//! struct cmac_entry {          // Python struct '<3L16s', size 28
8//!     u32 enabled;             // 0x00000001 = active, 0xFFFFFFFF = unused
9//!     u32 start;               // file offset, inclusive
10//!     u32 end;                 // file offset, inclusive
11//!     u8  cmac[16];            // stored digest (byte-reversed, see below)
12//! };
13//! ```
14//!
15//! Digest recipe (reproduced byte-for-byte against real stock images):
16//! 1. Read bytes `[start ..= end]`.
17//! 2. Byte-reverse each 16-byte block in place.
18//! 3. `digest = AES-CMAC(KEY, reversed_data)`.
19//! 4. Byte-reverse the 16-byte digest and store it at `entry + 12`.
20//!
21//! The CMAC key is symmetric and public (integrity, not confidentiality):
22//! `BD209408E35E526A36235234434FE8AB`. Anyone with the key can re-sign; the MAC
23//! exists to reject accidental corruption and unsigned third-party images.
24//!
25//! Re-sign ordering matters: the entry whose range covers the table region
26//! (`0x10400`) hashes the *other* entries' digest bytes, so it must be computed
27//! last, after every data range has been written back.
28
29use aes::Aes128;
30use cmac::{Cmac, Mac};
31
32/// Public AES-CMAC integrity key, shared by MT1959 and MT1939 firmware.
33pub const CMAC_KEY: [u8; 16] = [
34    0xBD, 0x20, 0x94, 0x08, 0xE3, 0x5E, 0x52, 0x6A, 0x36, 0x23, 0x52, 0x34, 0x43, 0x4F, 0xE8, 0xAB,
35];
36
37/// File offset of the integrity table.
38pub const TABLE_OFFSET: usize = crate::layout::CMAC_TABLE.start;
39/// Number of table entries.
40pub const ENTRY_COUNT: usize = 16;
41/// Size of a single table entry in bytes.
42pub const ENTRY_SIZE: usize = 28;
43/// `enabled` sentinel for an active entry.
44pub const ENABLED: u32 = 0x0000_0001;
45
46/// A parsed integrity-table entry.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct CmacEntry {
49    /// Index within the table (0..16).
50    pub index: usize,
51    /// File offset of this entry's 28 bytes.
52    pub entry_off: usize,
53    /// `enabled` word.
54    pub enabled: u32,
55    /// Inclusive start file offset of the covered range.
56    pub start: u32,
57    /// Inclusive end file offset of the covered range.
58    pub end: u32,
59    /// Stored (byte-reversed) digest as it sits in the file.
60    pub stored: [u8; 16],
61}
62
63impl CmacEntry {
64    /// Whether this entry participates in verification / signing.
65    pub fn is_active(&self) -> bool {
66        self.enabled == ENABLED
67    }
68
69    /// Whether this entry's covered range overlaps the integrity table itself.
70    pub fn covers_table(&self) -> bool {
71        let table_end = TABLE_OFFSET + ENTRY_COUNT * ENTRY_SIZE;
72        (self.start as usize) < table_end && (self.end as usize) >= TABLE_OFFSET
73    }
74}
75
76fn read_u32_le(buf: &[u8], off: usize) -> u32 {
77    u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
78}
79
80/// Parse all 16 table entries. Returns an error if the image is too small.
81pub fn parse_table(image: &[u8]) -> Result<Vec<CmacEntry>, CmacError> {
82    let table_end = TABLE_OFFSET + ENTRY_COUNT * ENTRY_SIZE;
83    if image.len() < table_end {
84        return Err(CmacError::ImageTooSmall {
85            need: table_end,
86            got: image.len(),
87        });
88    }
89    let mut out = Vec::with_capacity(ENTRY_COUNT);
90    for index in 0..ENTRY_COUNT {
91        let entry_off = TABLE_OFFSET + index * ENTRY_SIZE;
92        let enabled = read_u32_le(image, entry_off);
93        let start = read_u32_le(image, entry_off + 4);
94        let end = read_u32_le(image, entry_off + 8);
95        let mut stored = [0u8; 16];
96        stored.copy_from_slice(&image[entry_off + 12..entry_off + 28]);
97        out.push(CmacEntry {
98            index,
99            entry_off,
100            enabled,
101            start,
102            end,
103            stored,
104        });
105    }
106    Ok(out)
107}
108
109/// Reverse each 16-byte block of `data` in place (final short block reversed too).
110fn reverse_blocks(data: &mut [u8]) {
111    for chunk in data.chunks_mut(16) {
112        chunk.reverse();
113    }
114}
115
116/// Compute the stored-form (byte-reversed) digest for a covered range.
117///
118/// Returns the 16 bytes exactly as they should appear in the file at `entry+12`.
119pub fn compute_stored_digest(image: &[u8], start: u32, end: u32) -> Result<[u8; 16], CmacError> {
120    let start = start as usize;
121    let end = end as usize;
122    if end < start || end >= image.len() {
123        return Err(CmacError::BadRange { start, end });
124    }
125    let mut mac = <Cmac<Aes128> as Mac>::new_from_slice(&CMAC_KEY)
126        .expect("AES-128 key length is always valid for CMAC");
127    // Pieces are a multiple of 16 bytes from `start`, so each 16-byte reversal
128    // block (and the short final one) is exactly as in the whole range.
129    let mut buf = [0u8; 1024];
130    for piece in image[start..=end].chunks(buf.len()) {
131        let part = &mut buf[..piece.len()];
132        part.copy_from_slice(piece);
133        reverse_blocks(part);
134        mac.update(part);
135    }
136    let tag = mac.finalize().into_bytes();
137
138    let mut digest = [0u8; 16];
139    digest.copy_from_slice(&tag);
140    digest.reverse(); // step 4: byte-reverse the digest before storing
141    Ok(digest)
142}
143
144/// Result of verifying one entry.
145#[derive(Debug, Clone, Copy)]
146pub struct EntryVerdict {
147    /// The entry that was checked.
148    pub entry: CmacEntry,
149    /// Freshly computed stored-form digest.
150    pub computed: [u8; 16],
151    /// Whether `computed == entry.stored`.
152    pub matches: bool,
153}
154
155/// Verify every active entry. Returns a per-entry verdict list.
156pub fn verify_detailed(image: &[u8]) -> Result<Vec<EntryVerdict>, CmacError> {
157    let table = parse_table(image)?;
158    let mut verdicts = Vec::new();
159    for entry in table {
160        if !entry.is_active() {
161            continue;
162        }
163        let computed = compute_stored_digest(image, entry.start, entry.end)?;
164        verdicts.push(EntryVerdict {
165            matches: computed == entry.stored,
166            entry,
167            computed,
168        });
169    }
170    Ok(verdicts)
171}
172
173/// Return `true` iff every active entry's stored digest matches a fresh compute.
174pub fn verify(image: &[u8]) -> bool {
175    match verify_detailed(image) {
176        Ok(v) => !v.is_empty() && v.iter().all(|e| e.matches),
177        Err(_) => false,
178    }
179}
180
181/// Recompute and write back every active entry's digest, returning the new image.
182///
183/// Data ranges are signed first; any entry that covers the table region
184/// (`0x10400`) is signed last, over the already-updated table bytes.
185pub fn resign(image: &[u8]) -> Result<Vec<u8>, CmacError> {
186    let mut img = image.to_vec();
187    let table = parse_table(&img)?;
188
189    // Pass 1: data ranges (those that do not cover the table).
190    for entry in table.iter().filter(|e| e.is_active() && !e.covers_table()) {
191        let digest = compute_stored_digest(&img, entry.start, entry.end)?;
192        img[entry.entry_off + 12..entry.entry_off + 28].copy_from_slice(&digest);
193    }
194    // Pass 2: table-covering ranges, over the now-updated table bytes.
195    for entry in table.iter().filter(|e| e.is_active() && e.covers_table()) {
196        let digest = compute_stored_digest(&img, entry.start, entry.end)?;
197        img[entry.entry_off + 12..entry.entry_off + 28].copy_from_slice(&digest);
198    }
199    Ok(img)
200}
201
202/// Errors from the CMAC layer.
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204#[non_exhaustive]
205pub enum CmacError {
206    /// Image is smaller than the integrity table requires.
207    ImageTooSmall {
208        /// Required minimum length.
209        need: usize,
210        /// Actual length.
211        got: usize,
212    },
213    /// A table entry described an out-of-bounds or inverted range.
214    BadRange {
215        /// Range start.
216        start: usize,
217        /// Range end.
218        end: usize,
219    },
220}
221
222impl std::fmt::Display for CmacError {
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224        match self {
225            CmacError::ImageTooSmall { need, got } => {
226                write!(f, "image too small: need {need} bytes, got {got}")
227            }
228            CmacError::BadRange { start, end } => {
229                write!(f, "bad CMAC range: start=0x{start:x} end=0x{end:x}")
230            }
231        }
232    }
233}
234
235impl std::error::Error for CmacError {}