use aes::{Aes256, cipher::KeyInit};
use cipher::{BlockDecryptMut, BlockEncryptMut, BlockSizeUser, block_padding::NoPadding};
use ecb::{Decryptor, Encryptor};
use miette::Diagnostic;
use std::fmt::{Display, Formatter, Result as FmtResult};
use thiserror::Error;
type Aes256EcbEnc = Encryptor<Aes256>;
type Aes256EcbDec = Decryptor<Aes256>;
const STOCK_FW_KEY: [u8; 32] = [
0xA9, 0xFE, 0x4F, 0x78, 0x26, 0x3A, 0xE0, 0xE0, 0xC8, 0xFF, 0x39, 0x95, 0xE4, 0x43, 0x1F, 0x74,
0x87, 0x9D, 0x1C, 0x67, 0x04, 0x29, 0xBC, 0x79, 0xA5, 0xE3, 0x35, 0x47, 0x8A, 0x60, 0x3B, 0x22,
];
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum MionFirmwareType {
Fpga,
Ipl,
Mion,
}
impl Display for MionFirmwareType {
fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
match *self {
Self::Fpga => write!(fmt, "fpga"),
Self::Ipl => write!(fmt, "ipl"),
Self::Mion => write!(fmt, "fw"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MionFirmwareFile {
data: Vec<u8>,
fw_type: MionFirmwareType,
version_bytes: [u8; 4],
unk_byte: u8,
checksum: u8,
}
impl MionFirmwareFile {
pub fn parse(
firmware: &[u8],
firmware_type: MionFirmwareType,
) -> Result<Self, MionFirmwareAPIError> {
let firmware_length = firmware.len();
if firmware_length < 0x26 {
return Err(MionFirmwareAPIError::TooSmall(firmware_length));
}
let chksum_in_file = firmware[firmware_length - 1];
let got_chksum = calculate_checksum(firmware);
if chksum_in_file != got_chksum {
return Err(MionFirmwareAPIError::BadChecksum(
chksum_in_file,
got_chksum,
));
}
if matches!(firmware_type, MionFirmwareType::Mion) && firmware[firmware_length - 3] != 0x00
{
return Err(MionFirmwareAPIError::MissingNULTerminator(
firmware[firmware_length - 3],
));
}
let decrypted = raw_decrypt(&firmware[..firmware_length - 6])?;
let expected_footer = match firmware_type {
MionFirmwareType::Fpga => b"PWI-SS_FP_IMAGE\0",
_ => b"PWI-SS_FW_IMAGE\0",
};
if !decrypted.ends_with(expected_footer) {
return Err(MionFirmwareAPIError::MissingSignature);
}
Ok(Self {
data: decrypted,
fw_type: firmware_type,
version_bytes: [
firmware[firmware_length - 6],
firmware[firmware_length - 5],
firmware[firmware_length - 4],
firmware[firmware_length - 3],
],
unk_byte: firmware[firmware_length - 2],
checksum: chksum_in_file,
})
}
#[must_use]
pub const fn checksum(&self) -> u8 {
self.checksum
}
#[must_use]
pub const fn contents(&self) -> &Vec<u8> {
&self.data
}
#[must_use]
pub const fn firmware_type(&self) -> MionFirmwareType {
MionFirmwareType::Mion
}
#[allow(
// This function can't actually panic, it's just raw_encrypt doesn't know
// we've pre-validated the length by decrypting it successfully and not
// offering mutable APIs.
clippy::missing_panics_doc,
)]
#[must_use]
pub fn get_deployable_firmware_data(&self) -> Vec<u8> {
let mut encrypted =
raw_encrypt(&self.data).expect("We validate the block size at parse time.");
encrypted.extend_from_slice(&self.version_bytes);
encrypted.push(self.unk_byte);
encrypted.push(self.checksum);
encrypted
}
#[must_use]
pub fn version(&self) -> String {
match self.fw_type {
MionFirmwareType::Mion => format!(
"0.{:02}.{}.{}",
self.version_bytes[0], self.version_bytes[1], self.version_bytes[2],
),
MionFirmwareType::Fpga => format!(
"{:02X}{:02X}{:02X}{:02X}",
self.version_bytes[3],
self.version_bytes[2],
self.version_bytes[1],
self.version_bytes[0],
),
MionFirmwareType::Ipl => format!(
"{}.{}",
u16::from_le_bytes([self.version_bytes[0], self.version_bytes[1]]),
u16::from_le_bytes([self.version_bytes[2], self.version_bytes[3]]),
),
}
}
}
#[derive(Error, Diagnostic, Debug, PartialEq, Eq)]
pub enum MionFirmwareAPIError {
#[error(
"We could not encrypt your data, because it was not padded to the correct length, expected a block size of: {0}"
)]
#[diagnostic(code(cat_dev::api::mion::firmware::bad_decrypted_data_length))]
BadDecryptedDataLength(usize),
#[error(
"We could not decrypt your data, because it was not padded to the correct length, expected a block size of: {0}"
)]
#[diagnostic(code(cat_dev::api::mion::firmware::bad_encrypted_data_length))]
BadEncryptedDataLength(usize),
#[error(
"The MION Firmware file you provided had an invalid checksum, we expected: {1:02x}, but got: {0:02x}"
)]
#[diagnostic(code(cat_dev::api::mion::firmware::bad_checksum))]
BadChecksum(u8, u8),
#[error(
"The MION Firmware file provided was too small, it must be at least 0x26 bytes long, was {0:02x}"
)]
#[diagnostic(code(cat_dev::api::mion::firmware::too_small))]
TooSmall(usize),
#[error(
"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})"
)]
#[diagnostic(code(cat_dev::api::mion::firmware::missing_nul_terminator))]
MissingNULTerminator(u8),
#[error(
"While validating the decrypted contents of your FW we were not able to identify the required ending bytes, this firmware is corrupt."
)]
#[diagnostic(code(cat_dev::api::mion_fw::missing_signature))]
MissingSignature,
}
fn calculate_checksum(encrypted_blob: &[u8]) -> u8 {
let mut chksum = 0_u32;
for byte in encrypted_blob.iter().take(encrypted_blob.len() - 1) {
chksum = chksum.wrapping_add((*byte).into());
}
while chksum & 0xFFFF_FF00_u32 != 0 {
chksum = (chksum & 0xFF) + (chksum >> 8);
}
u8::try_from(!chksum & 0xFF)
.expect("&0xFF did not just give us the last 8 bits??? is math broken?")
}
#[doc(hidden)]
pub fn raw_encrypt(file_contents: &[u8]) -> Result<Vec<u8>, MionFirmwareAPIError> {
let encryptor = Aes256EcbEnc::new(&STOCK_FW_KEY.into());
let mut decrypted = vec![
0x0;
Aes256EcbEnc::block_size()
* (file_contents.len() / Aes256EcbEnc::block_size() + 1)
];
let actual_len = encryptor
.encrypt_padded_b2b_mut::<NoPadding>(file_contents, &mut decrypted)
.map_err(|_| MionFirmwareAPIError::BadDecryptedDataLength(Aes256EcbEnc::block_size()))?
.len();
decrypted.truncate(actual_len);
Ok(decrypted)
}
#[doc(hidden)]
pub fn raw_decrypt(file_contents: &[u8]) -> Result<Vec<u8>, MionFirmwareAPIError> {
let decryptor = Aes256EcbDec::new(&STOCK_FW_KEY.into());
decryptor
.decrypt_padded_vec_mut::<NoPadding>(file_contents)
.map_err(|_| MionFirmwareAPIError::BadEncryptedDataLength(Aes256EcbDec::block_size()))
}
#[cfg(test)]
mod unit_tests {
use super::*;
use std::path::PathBuf;
#[must_use]
pub fn get_test_data_path(relative_to_test_data: &str) -> PathBuf {
let mut final_path = PathBuf::from(
std::env::var("CARGO_MANIFEST_DIR")
.expect("Failed to read `CARGO_MANIFEST_DIR` to locate t est files!"),
);
final_path.push("src");
final_path.push("mion");
final_path.push("test-data");
for file_part in relative_to_test_data.split('/') {
if file_part.is_empty() {
continue;
}
final_path.push(file_part);
}
final_path
}
#[test]
pub fn can_decrypt_and_reencrypt_fw() {
for (source_file_name, dest_file_name) in vec![
("/fpga.13052071.bin", "/fpga.13052071_d.bin"),
("/fw.0.00.14.80.bin", "/fw.0.00.14.80_d.bin"),
("/ipl.0.5.bin", "/ipl.0.5_d.bin"),
] {
let encrypted_path = get_test_data_path(source_file_name);
let decrypted_path = get_test_data_path(dest_file_name);
let full_encrypted_contents =
std::fs::read(&encrypted_path).expect("Failed to read encrypted file to decrypt!");
let decrypted =
raw_decrypt(&full_encrypted_contents[..]).expect("Failed to decrypt data!");
let expected_decrypted_contents = std::fs::read(&decrypted_path)
.expect("Failed to read expected decrypted contents!");
assert_eq!(
decrypted.len(),
expected_decrypted_contents.len(),
"Decrypted data length did not match expected decrypted data length, file: {}",
encrypted_path.display(),
);
for (idx, byte) in decrypted.iter().enumerate() {
if *byte != expected_decrypted_contents[idx] {
panic!(
"Decrypted Byte at Location: {idx} did not match expected contents! (total: {})",
decrypted.len(),
);
}
}
let re_encrypted = raw_encrypt(&decrypted).expect("Failed to encrypt firmware!");
assert_eq!(
re_encrypted.len(),
full_encrypted_contents.len(),
"Encrypted data length did not match expected encrypted data length!",
);
for (idx, byte) in re_encrypted.iter().enumerate() {
if *byte != full_encrypted_contents[idx] {
panic!(
"Re-Encrypted Byte at Location: {idx} did not match expected contents! (total: {})",
re_encrypted.len(),
);
}
}
}
}
#[test]
pub fn correctly_calculates_checksums() {
for (file_path, footer_data, name, expected_value) in vec![
(
"/fw.0.00.14.80.bin",
vec![0x00_u8, 0x0E, 0x50, 0x00, 0xA1, 0x00],
"$encrypted.0.14.80.0.$0xA1",
0x21_u8,
),
(
"/fw.0.00.14.80.bin",
vec![0x01_u8, 0x0E, 0x50, 0x00, 0xA1, 0x00],
"$encrypted.1.14.80.0.$0xA1",
0x20_u8,
),
(
"/fw.0.00.14.80.bin",
vec![0xFF_u8, 0x0E, 0x50, 0x00, 0xA1, 0x00],
"$encrypted.255.14.80.0.$0xA1",
0x21_u8,
),
(
"/fw.0.00.14.80.bin",
vec![0xCF_u8, 0x0E, 0x50, 0x00, 0xA1, 0x00],
"$encrypted.207.14.80.0.$0xA1",
0x51_u8,
),
] {
let mut encrypted_contents = std::fs::read(get_test_data_path(file_path))
.expect("Failed to read test data file!");
encrypted_contents.extend(footer_data);
assert_eq!(
calculate_checksum(&encrypted_contents),
expected_value,
"Checksum did not match for named contents: {name}! Please check checksum code!",
);
}
}
#[test]
pub fn can_successfully_parse_real_fw_file() {
let mut mion_fw = std::fs::read(get_test_data_path("/fw.0.00.14.80.bin"))
.expect("Failed to read encrypted MION FW!");
let mut ipl_fw = std::fs::read(get_test_data_path("/ipl.0.5.bin"))
.expect("Failed to read encrypted IPL FW!");
let mut fpga_fw = std::fs::read(get_test_data_path("/fpga.13052071.bin"))
.expect("Failed to read encrypted FPGA FW!");
mion_fw.extend([0x00, 0x0E, 0x50, 0x00, 0xA1, 0x21]);
ipl_fw.extend([0x00, 0x00, 0x05, 0x00, 0xA1, 0x3E]);
fpga_fw.extend([0x71, 0x20, 0x05, 0x13, 0xA2, 0xBF]);
let parsed_mion = MionFirmwareFile::parse(&mion_fw, MionFirmwareType::Mion)
.expect("Failed to parse MION firmware!");
let parsed_ipl = MionFirmwareFile::parse(&ipl_fw, MionFirmwareType::Ipl)
.expect("Failed to parse IPL firmware!");
let parsed_fpga = MionFirmwareFile::parse(&fpga_fw, MionFirmwareType::Fpga)
.expect("Failed to parse FPGA firmware!");
assert_eq!(parsed_mion.version(), "0.00.14.80");
assert_eq!(parsed_ipl.version(), "0.5");
assert_eq!(parsed_fpga.version(), "13052071");
}
}