1use aes::{Aes256, cipher::KeyInit};
4use cipher::{BlockDecryptMut, BlockEncryptMut, BlockSizeUser, block_padding::NoPadding};
5use ecb::{Decryptor, Encryptor};
6use miette::Diagnostic;
7use std::fmt::{Display, Formatter, Result as FmtResult};
8use thiserror::Error;
9
10type Aes256EcbEnc = Encryptor<Aes256>;
11type Aes256EcbDec = Decryptor<Aes256>;
12
13const STOCK_FW_KEY: [u8; 32] = [
15 0xA9, 0xFE, 0x4F, 0x78, 0x26, 0x3A, 0xE0, 0xE0, 0xC8, 0xFF, 0x39, 0x95, 0xE4, 0x43, 0x1F, 0x74,
16 0x87, 0x9D, 0x1C, 0x67, 0x04, 0x29, 0xBC, 0x79, 0xA5, 0xE3, 0x35, 0x47, 0x8A, 0x60, 0x3B, 0x22,
17];
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub enum MionFirmwareType {
23 Fpga,
25 Ipl,
28 Mion,
30}
31impl Display for MionFirmwareType {
32 fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
33 match *self {
34 Self::Fpga => write!(fmt, "fpga"),
35 Self::Ipl => write!(fmt, "ipl"),
36 Self::Mion => write!(fmt, "fw"),
37 }
38 }
39}
40
41#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct MionFirmwareFile {
54 data: Vec<u8>,
57 fw_type: MionFirmwareType,
59 version_bytes: [u8; 4],
64 unk_byte: u8,
66 checksum: u8,
68}
69
70impl MionFirmwareFile {
71 pub fn parse(
100 firmware: &[u8],
101 firmware_type: MionFirmwareType,
102 ) -> Result<Self, MionFirmwareAPIError> {
103 let firmware_length = firmware.len();
104 if firmware_length < 0x26 {
105 return Err(MionFirmwareAPIError::TooSmall(firmware_length));
106 }
107 let chksum_in_file = firmware[firmware_length - 1];
108 let got_chksum = calculate_checksum(firmware);
109 if chksum_in_file != got_chksum {
110 return Err(MionFirmwareAPIError::BadChecksum(
111 chksum_in_file,
112 got_chksum,
113 ));
114 }
115 if matches!(firmware_type, MionFirmwareType::Mion) && firmware[firmware_length - 3] != 0x00
120 {
121 return Err(MionFirmwareAPIError::MissingNULTerminator(
122 firmware[firmware_length - 3],
123 ));
124 }
125 let decrypted = raw_decrypt(&firmware[..firmware_length - 6])?;
126
127 let expected_footer = match firmware_type {
128 MionFirmwareType::Fpga => b"PWI-SS_FP_IMAGE\0",
129 _ => b"PWI-SS_FW_IMAGE\0",
130 };
131 if !decrypted.ends_with(expected_footer) {
132 return Err(MionFirmwareAPIError::MissingSignature);
133 }
134
135 Ok(Self {
136 data: decrypted,
137 fw_type: firmware_type,
138 version_bytes: [
139 firmware[firmware_length - 6],
140 firmware[firmware_length - 5],
141 firmware[firmware_length - 4],
142 firmware[firmware_length - 3],
143 ],
144 unk_byte: firmware[firmware_length - 2],
145 checksum: chksum_in_file,
146 })
147 }
148
149 #[must_use]
151 pub const fn checksum(&self) -> u8 {
152 self.checksum
153 }
154
155 #[must_use]
157 pub const fn contents(&self) -> &Vec<u8> {
158 &self.data
159 }
160
161 #[must_use]
163 pub const fn firmware_type(&self) -> MionFirmwareType {
164 MionFirmwareType::Mion
165 }
166
167 #[allow(
170 clippy::missing_panics_doc,
174 )]
175 #[must_use]
176 pub fn get_deployable_firmware_data(&self) -> Vec<u8> {
177 let mut encrypted =
178 raw_encrypt(&self.data).expect("We validate the block size at parse time.");
179 encrypted.extend_from_slice(&self.version_bytes);
180 encrypted.push(self.unk_byte);
181 encrypted.push(self.checksum);
182 encrypted
183 }
184
185 #[must_use]
194 pub fn version(&self) -> String {
195 match self.fw_type {
196 MionFirmwareType::Mion => format!(
197 "0.{:02}.{}.{}",
198 self.version_bytes[0], self.version_bytes[1], self.version_bytes[2],
199 ),
200 MionFirmwareType::Fpga => format!(
201 "{:02X}{:02X}{:02X}{:02X}",
202 self.version_bytes[3],
203 self.version_bytes[2],
204 self.version_bytes[1],
205 self.version_bytes[0],
206 ),
207 MionFirmwareType::Ipl => format!(
208 "{}.{}",
209 u16::from_le_bytes([self.version_bytes[0], self.version_bytes[1]]),
210 u16::from_le_bytes([self.version_bytes[2], self.version_bytes[3]]),
211 ),
212 }
213 }
214}
215
216#[derive(Error, Diagnostic, Debug, PartialEq, Eq)]
218pub enum MionFirmwareAPIError {
219 #[error(
221 "We could not encrypt your data, because it was not padded to the correct length, expected a block size of: {0}"
222 )]
223 #[diagnostic(code(cat_dev::api::mion::firmware::bad_decrypted_data_length))]
224 BadDecryptedDataLength(usize),
225 #[error(
227 "We could not decrypt your data, because it was not padded to the correct length, expected a block size of: {0}"
228 )]
229 #[diagnostic(code(cat_dev::api::mion::firmware::bad_encrypted_data_length))]
230 BadEncryptedDataLength(usize),
231 #[error(
235 "The MION Firmware file you provided had an invalid checksum, we expected: {1:02x}, but got: {0:02x}"
236 )]
237 #[diagnostic(code(cat_dev::api::mion::firmware::bad_checksum))]
238 BadChecksum(u8, u8),
239 #[error(
244 "The MION Firmware file provided was too small, it must be at least 0x26 bytes long, was {0:02x}"
245 )]
246 #[diagnostic(code(cat_dev::api::mion::firmware::too_small))]
247 TooSmall(usize),
248 #[error(
251 "The Version String for MION Firmware Files Typed 'MION', must have their version bytes end with a NUL terminator (0x00) due to an oversight in programming. Your file ended with: ({0:02x})"
252 )]
253 #[diagnostic(code(cat_dev::api::mion::firmware::missing_nul_terminator))]
254 MissingNULTerminator(u8),
255 #[error(
262 "While validating the decrypted contents of your FW we were not able to identify the required ending bytes, this firmware is corrupt."
263 )]
264 #[diagnostic(code(cat_dev::api::mion_fw::missing_signature))]
265 MissingSignature,
266}
267
268fn calculate_checksum(encrypted_blob: &[u8]) -> u8 {
280 let mut chksum = 0_u32;
281 for byte in encrypted_blob.iter().take(encrypted_blob.len() - 1) {
284 chksum = chksum.wrapping_add((*byte).into());
285 }
286 while chksum & 0xFFFF_FF00_u32 != 0 {
287 chksum = (chksum & 0xFF) + (chksum >> 8);
288 }
289 u8::try_from(!chksum & 0xFF)
290 .expect("&0xFF did not just give us the last 8 bits??? is math broken?")
291}
292
293#[doc(hidden)]
300pub fn raw_encrypt(file_contents: &[u8]) -> Result<Vec<u8>, MionFirmwareAPIError> {
301 let encryptor = Aes256EcbEnc::new(&STOCK_FW_KEY.into());
302 let mut decrypted = vec![
303 0x0;
304 Aes256EcbEnc::block_size()
305 * (file_contents.len() / Aes256EcbEnc::block_size() + 1)
306 ];
307 let actual_len = encryptor
308 .encrypt_padded_b2b_mut::<NoPadding>(file_contents, &mut decrypted)
309 .map_err(|_| MionFirmwareAPIError::BadDecryptedDataLength(Aes256EcbEnc::block_size()))?
310 .len();
311 decrypted.truncate(actual_len);
312 Ok(decrypted)
313}
314
315#[doc(hidden)]
321pub fn raw_decrypt(file_contents: &[u8]) -> Result<Vec<u8>, MionFirmwareAPIError> {
322 let decryptor = Aes256EcbDec::new(&STOCK_FW_KEY.into());
323
324 decryptor
325 .decrypt_padded_vec_mut::<NoPadding>(file_contents)
326 .map_err(|_| MionFirmwareAPIError::BadEncryptedDataLength(Aes256EcbDec::block_size()))
327}
328
329#[cfg(test)]
330mod unit_tests {
331 use super::*;
332 use std::path::PathBuf;
333
334 #[must_use]
335 pub fn get_test_data_path(relative_to_test_data: &str) -> PathBuf {
336 let mut final_path = PathBuf::from(
337 std::env::var("CARGO_MANIFEST_DIR")
338 .expect("Failed to read `CARGO_MANIFEST_DIR` to locate t est files!"),
339 );
340 final_path.push("src");
341 final_path.push("mion");
342 final_path.push("test-data");
343 for file_part in relative_to_test_data.split('/') {
344 if file_part.is_empty() {
345 continue;
346 }
347 final_path.push(file_part);
348 }
349 final_path
350 }
351
352 #[test]
353 pub fn can_decrypt_and_reencrypt_fw() {
354 for (source_file_name, dest_file_name) in vec![
355 ("/fpga.13052071.bin", "/fpga.13052071_d.bin"),
356 ("/fw.0.00.14.80.bin", "/fw.0.00.14.80_d.bin"),
357 ("/ipl.0.5.bin", "/ipl.0.5_d.bin"),
358 ] {
359 let encrypted_path = get_test_data_path(source_file_name);
360 let decrypted_path = get_test_data_path(dest_file_name);
361
362 let full_encrypted_contents =
363 std::fs::read(&encrypted_path).expect("Failed to read encrypted file to decrypt!");
364 let decrypted =
365 raw_decrypt(&full_encrypted_contents[..]).expect("Failed to decrypt data!");
366 let expected_decrypted_contents = std::fs::read(&decrypted_path)
367 .expect("Failed to read expected decrypted contents!");
368
369 assert_eq!(
370 decrypted.len(),
371 expected_decrypted_contents.len(),
372 "Decrypted data length did not match expected decrypted data length, file: {}",
373 encrypted_path.display(),
374 );
375 for (idx, byte) in decrypted.iter().enumerate() {
376 if *byte != expected_decrypted_contents[idx] {
377 panic!(
378 "Decrypted Byte at Location: {idx} did not match expected contents! (total: {})",
379 decrypted.len(),
380 );
381 }
382 }
383
384 let re_encrypted = raw_encrypt(&decrypted).expect("Failed to encrypt firmware!");
385 assert_eq!(
386 re_encrypted.len(),
387 full_encrypted_contents.len(),
388 "Encrypted data length did not match expected encrypted data length!",
389 );
390 for (idx, byte) in re_encrypted.iter().enumerate() {
391 if *byte != full_encrypted_contents[idx] {
392 panic!(
393 "Re-Encrypted Byte at Location: {idx} did not match expected contents! (total: {})",
394 re_encrypted.len(),
395 );
396 }
397 }
398 }
399 }
400
401 #[test]
402 pub fn correctly_calculates_checksums() {
403 for (file_path, footer_data, name, expected_value) in vec![
437 (
438 "/fw.0.00.14.80.bin",
439 vec![0x00_u8, 0x0E, 0x50, 0x00, 0xA1, 0x00],
440 "$encrypted.0.14.80.0.$0xA1",
441 0x21_u8,
442 ),
443 (
444 "/fw.0.00.14.80.bin",
445 vec![0x01_u8, 0x0E, 0x50, 0x00, 0xA1, 0x00],
446 "$encrypted.1.14.80.0.$0xA1",
447 0x20_u8,
448 ),
449 (
450 "/fw.0.00.14.80.bin",
451 vec![0xFF_u8, 0x0E, 0x50, 0x00, 0xA1, 0x00],
452 "$encrypted.255.14.80.0.$0xA1",
453 0x21_u8,
454 ),
455 (
456 "/fw.0.00.14.80.bin",
457 vec![0xCF_u8, 0x0E, 0x50, 0x00, 0xA1, 0x00],
458 "$encrypted.207.14.80.0.$0xA1",
459 0x51_u8,
460 ),
461 ] {
462 let mut encrypted_contents = std::fs::read(get_test_data_path(file_path))
463 .expect("Failed to read test data file!");
464 encrypted_contents.extend(footer_data);
465 assert_eq!(
466 calculate_checksum(&encrypted_contents),
467 expected_value,
468 "Checksum did not match for named contents: {name}! Please check checksum code!",
469 );
470 }
471 }
472
473 #[test]
474 pub fn can_successfully_parse_real_fw_file() {
475 let mut mion_fw = std::fs::read(get_test_data_path("/fw.0.00.14.80.bin"))
476 .expect("Failed to read encrypted MION FW!");
477 let mut ipl_fw = std::fs::read(get_test_data_path("/ipl.0.5.bin"))
478 .expect("Failed to read encrypted IPL FW!");
479 let mut fpga_fw = std::fs::read(get_test_data_path("/fpga.13052071.bin"))
480 .expect("Failed to read encrypted FPGA FW!");
481 mion_fw.extend([0x00, 0x0E, 0x50, 0x00, 0xA1, 0x21]);
483 ipl_fw.extend([0x00, 0x00, 0x05, 0x00, 0xA1, 0x3E]);
484 fpga_fw.extend([0x71, 0x20, 0x05, 0x13, 0xA2, 0xBF]);
485
486 let parsed_mion = MionFirmwareFile::parse(&mion_fw, MionFirmwareType::Mion)
487 .expect("Failed to parse MION firmware!");
488 let parsed_ipl = MionFirmwareFile::parse(&ipl_fw, MionFirmwareType::Ipl)
489 .expect("Failed to parse IPL firmware!");
490 let parsed_fpga = MionFirmwareFile::parse(&fpga_fw, MionFirmwareType::Fpga)
491 .expect("Failed to parse FPGA firmware!");
492
493 assert_eq!(parsed_mion.version(), "0.00.14.80");
494 assert_eq!(parsed_ipl.version(), "0.5");
495 assert_eq!(parsed_fpga.version(), "13052071");
496 }
497}