mirror of
https://gitea.tendokyu.moe/beerpsi/icf-reader.git
synced 2026-10-13 01:38:23 +03:00
Initial commit
This commit is contained in:
+365
@@ -0,0 +1,365 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use aes::cipher::{block_padding::NoPadding, BlockDecryptMut, KeyIvInit, BlockEncryptMut};
|
||||
use anyhow::{anyhow, Result};
|
||||
use binary_reader::{BinaryReader, Endian};
|
||||
use chrono::{NaiveDate, NaiveDateTime};
|
||||
|
||||
type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
|
||||
type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
|
||||
|
||||
pub const ICF_KEY: [u8; 16] = hex_literal::decode(&[env!("ICF_KEY").as_bytes()]);
|
||||
pub const ICF_IV: [u8; 16] = hex_literal::decode(&[env!("ICF_IV").as_bytes()]);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Version {
|
||||
pub major: u16,
|
||||
pub minor: u8,
|
||||
pub build: u8,
|
||||
}
|
||||
|
||||
impl Display for Version {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}.{:0>2}.{:0>2}", self.major, self.minor, self.build)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IcfInnerData {
|
||||
pub id: String,
|
||||
pub version: Version,
|
||||
pub required_system_version: Version,
|
||||
pub datetime: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IcfOptionData {
|
||||
pub app_id: String,
|
||||
pub option_id: String,
|
||||
pub required_system_version: Version,
|
||||
pub datetime: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IcfPatchData {
|
||||
pub id: String,
|
||||
pub source_version: Version,
|
||||
pub target_version: Version,
|
||||
pub required_system_version: Version,
|
||||
pub datetime: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum IcfData {
|
||||
System(IcfInnerData),
|
||||
App(IcfInnerData),
|
||||
Patch(IcfPatchData),
|
||||
Option(IcfOptionData),
|
||||
}
|
||||
|
||||
pub fn decrypt_icf(data: &mut [u8], key: impl AsRef<[u8]>, iv: impl AsRef<[u8]>) -> Result<Vec<u8>> {
|
||||
let size = data.len();
|
||||
|
||||
let mut decrypted = Vec::with_capacity(size);
|
||||
|
||||
for i in (0..size).step_by(4096) {
|
||||
let from_start = i;
|
||||
|
||||
let bufsz = std::cmp::min(4096, size - from_start);
|
||||
let buf = &data[i..i + bufsz];
|
||||
let mut decbuf = vec![0; bufsz];
|
||||
|
||||
let cipher = Aes128CbcDec::new_from_slices(key.as_ref(), iv.as_ref())?;
|
||||
cipher
|
||||
.decrypt_padded_b2b_mut::<NoPadding>(buf, &mut decbuf)
|
||||
.map_err(|err| anyhow!(err))?;
|
||||
|
||||
let xor1 = u64::from_le_bytes(decbuf[0..8].try_into()?) ^ (from_start as u64);
|
||||
let xor2 = u64::from_le_bytes(decbuf[8..16].try_into()?) ^ (from_start as u64);
|
||||
|
||||
decrypted.extend(xor1.to_le_bytes());
|
||||
decrypted.extend(xor2.to_le_bytes());
|
||||
decrypted.extend(&decbuf[16..]);
|
||||
}
|
||||
|
||||
Ok(decrypted)
|
||||
}
|
||||
|
||||
pub fn encrypt_icf(data: &[u8], key: impl AsRef<[u8]>, iv: impl AsRef<[u8]>) -> Result<Vec<u8>> {
|
||||
let size = data.len();
|
||||
|
||||
let mut encrypted = Vec::with_capacity(size);
|
||||
|
||||
for i in (0..size).step_by(4096) {
|
||||
let from_start = i;
|
||||
|
||||
let bufsz = std::cmp::min(4096, size - from_start);
|
||||
let buf = &data[i..i + bufsz];
|
||||
let mut to_be_encrypted = Vec::with_capacity(bufsz);
|
||||
let mut encbuf = vec![0; bufsz];
|
||||
|
||||
let xor1 = u64::from_le_bytes(buf[0..8].try_into()?) ^ (from_start as u64);
|
||||
let xor2 = u64::from_le_bytes(buf[8..16].try_into()?) ^ (from_start as u64);
|
||||
|
||||
to_be_encrypted.extend(xor1.to_le_bytes());
|
||||
to_be_encrypted.extend(xor2.to_le_bytes());
|
||||
to_be_encrypted.extend(&buf[16..]);
|
||||
|
||||
let cipher = Aes128CbcEnc::new_from_slices(key.as_ref(), iv.as_ref())?;
|
||||
cipher
|
||||
.encrypt_padded_b2b_mut::<NoPadding>(&to_be_encrypted, &mut encbuf)
|
||||
.map_err(|err| anyhow!(err))?;
|
||||
|
||||
encrypted.extend(encbuf);
|
||||
}
|
||||
|
||||
Ok(encrypted)
|
||||
}
|
||||
|
||||
pub fn decode_icf_datetime_version(
|
||||
rd: &mut BinaryReader,
|
||||
) -> Result<(NaiveDateTime, Version)> {
|
||||
let datetime = NaiveDate::from_ymd_opt(
|
||||
rd.read_i16()? as i32,
|
||||
rd.read_u8()? as u32,
|
||||
rd.read_u8()? as u32,
|
||||
)
|
||||
.ok_or(anyhow!("Invalid date"))?
|
||||
.and_hms_milli_opt(
|
||||
rd.read_u8()? as u32,
|
||||
rd.read_u8()? as u32,
|
||||
rd.read_u8()? as u32,
|
||||
rd.read_u8()? as u32,
|
||||
)
|
||||
.ok_or(anyhow!("Invalid time"))?;
|
||||
|
||||
let required_system_version = Version {
|
||||
build: rd.read_u8()?,
|
||||
minor: rd.read_u8()?,
|
||||
major: rd.read_u16()?,
|
||||
};
|
||||
|
||||
Ok((datetime, required_system_version))
|
||||
}
|
||||
|
||||
pub fn decode_icf_version(
|
||||
rd: &mut BinaryReader,
|
||||
) -> Result<Version> {
|
||||
let version = Version {
|
||||
build: rd.read_u8()?,
|
||||
minor: rd.read_u8()?,
|
||||
major: rd.read_u16()?,
|
||||
};
|
||||
|
||||
Ok(version)
|
||||
}
|
||||
|
||||
/// Fixes incorrect CRC32s caused by hex editing the ICF
|
||||
pub fn fixup_icf(data: &mut [u8]) -> Result<()> {
|
||||
let mut rd = BinaryReader::from_u8(data);
|
||||
rd.endian = Endian::Little;
|
||||
|
||||
let reported_icf_crc = rd.read_u32()?;
|
||||
|
||||
let reported_size = rd.read_u32()?;
|
||||
let actual_size = data.len() as u32;
|
||||
if actual_size != reported_size {
|
||||
println!("[WARN] Reported size {reported_size} bytes does not match actual size {actual_size} bytes, automatically fixing");
|
||||
data[4..8].copy_from_slice(&actual_size.to_le_bytes());
|
||||
}
|
||||
|
||||
let padding = rd.read_u64()?;
|
||||
if padding != 0 {
|
||||
return Err(anyhow!("Padding error. Expected 8 NULL bytes."));
|
||||
}
|
||||
|
||||
let entry_count = rd.read_u64()?;
|
||||
let expected_size = 0x40 * (entry_count + 1);
|
||||
let actual_entry_count = if actual_size as u64 != expected_size {
|
||||
println!("[WARN] Expected size {expected_size} ({entry_count} entries) does not match actual size {actual_size}, automatically fixing");
|
||||
|
||||
let actual_entry_count = actual_size as u64 / 0x40 - 1;
|
||||
|
||||
data[16..24].copy_from_slice(&actual_entry_count.to_le_bytes());
|
||||
|
||||
actual_entry_count
|
||||
} else {
|
||||
entry_count
|
||||
};
|
||||
|
||||
let _ = String::from_utf8(rd.read_bytes(4)?.to_vec())?;
|
||||
let _ = String::from_utf8(rd.read_bytes(3)?.to_vec())?;
|
||||
let _ = rd.read_u8()?;
|
||||
|
||||
let reported_container_crc = rd.read_u32()?;
|
||||
let mut checksum = 0;
|
||||
for i in 1..=(actual_entry_count as usize) {
|
||||
let container = &data[0x40 * i..0x40 * (i + 1)];
|
||||
if container[0] == 2 && container[1] == 1 {
|
||||
checksum ^= crc32fast::hash(container);
|
||||
}
|
||||
}
|
||||
|
||||
if reported_container_crc != checksum {
|
||||
println!("[WARN] Reported container CRC32 ({reported_container_crc:02X}) does not match actual checksum ({checksum:02X}), automatically fixing");
|
||||
|
||||
data[32..36].copy_from_slice(&checksum.to_le_bytes());
|
||||
}
|
||||
|
||||
let icf_checksum = crc32fast::hash(&data[4..]);
|
||||
if icf_checksum != reported_icf_crc {
|
||||
println!("[WARN] Reported CRC32 ({reported_icf_crc:02X}) does not match actual checksum ({icf_checksum:02X}), automatically fixing");
|
||||
data[0..4].copy_from_slice(&icf_checksum.to_le_bytes());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse_icf(data: impl AsRef<[u8]>) -> Result<Vec<IcfData>> {
|
||||
let decrypted = data.as_ref();
|
||||
|
||||
let mut rd = BinaryReader::from_u8(decrypted);
|
||||
rd.endian = Endian::Little;
|
||||
|
||||
let checksum = crc32fast::hash(&decrypted[4..]);
|
||||
let reported_crc = rd.read_u32()?;
|
||||
if reported_crc != checksum {
|
||||
return Err(anyhow!(
|
||||
"Reported CRC32 ({reported_crc:02X}) does not match actual checksum ({checksum:02X})"
|
||||
));
|
||||
}
|
||||
|
||||
let reported_size = rd.read_u32()? as usize;
|
||||
let actual_size = decrypted.len();
|
||||
if actual_size != reported_size {
|
||||
return Err(anyhow!(
|
||||
"Reported size {reported_size} does not match actual size {actual_size}"
|
||||
));
|
||||
}
|
||||
|
||||
let padding = rd.read_u64()?;
|
||||
if padding != 0 {
|
||||
return Err(anyhow!("Padding error. Expected 8 NULL bytes."));
|
||||
}
|
||||
|
||||
let entry_count: usize = rd.read_u64()?.try_into()?;
|
||||
let expected_size = 0x40 * (entry_count + 1);
|
||||
if actual_size != expected_size {
|
||||
return Err(anyhow!("Expected size {expected_size} ({entry_count} entries) does not match actual size {actual_size}"));
|
||||
}
|
||||
|
||||
let app_id = String::from_utf8(rd.read_bytes(4)?.to_vec())?;
|
||||
let platform_id = String::from_utf8(rd.read_bytes(3)?.to_vec())?;
|
||||
let _platform_generation = rd.read_u8()?;
|
||||
|
||||
let reported_crc = rd.read_u32()?;
|
||||
let mut checksum = 0;
|
||||
for i in 1..=entry_count {
|
||||
let container = &decrypted[0x40 * i..0x40 * (i + 1)];
|
||||
if container[0] == 2 && container[1] == 1 {
|
||||
checksum ^= crc32fast::hash(container);
|
||||
}
|
||||
}
|
||||
|
||||
if reported_crc != checksum {
|
||||
return Err(anyhow!("Reported container CRC32 ({reported_crc:02X}) does not match actual checksum ({checksum:02X})"));
|
||||
}
|
||||
|
||||
for _ in 0..7 {
|
||||
if rd.read_u32()? != 0 {
|
||||
return Err(anyhow!("Padding error. Expected 28 NULL bytes."));
|
||||
}
|
||||
}
|
||||
|
||||
let mut entries: Vec<IcfData> = Vec::with_capacity(entry_count);
|
||||
for _ in 0..entry_count {
|
||||
let sig = rd.read_bytes(4)?;
|
||||
if sig[0] != 2 || sig[1] != 1 {
|
||||
return Err(anyhow!("Container does not start with signature (0x0102), byte {:#06x}", rd.pos));
|
||||
}
|
||||
|
||||
let container_type = rd.read_u32()?;
|
||||
for _ in 0..3 {
|
||||
if rd.read_u64()? != 0 {
|
||||
return Err(anyhow!("Padding error. Expected 24 NULL bytes."));
|
||||
}
|
||||
}
|
||||
|
||||
let data: IcfData = match container_type {
|
||||
0x0000 | 0x0001 => {
|
||||
let version = decode_icf_version(&mut rd)?;
|
||||
let (datetime, required_system_version) = decode_icf_datetime_version(&mut rd)?;
|
||||
|
||||
for _ in 0..2 {
|
||||
if rd.read_u64()? != 0 {
|
||||
return Err(anyhow!("Padding error. Expected 16 NULL bytes."));
|
||||
}
|
||||
}
|
||||
|
||||
match container_type {
|
||||
0x0000 => IcfData::System(IcfInnerData {
|
||||
id: platform_id.clone(),
|
||||
version,
|
||||
datetime,
|
||||
required_system_version,
|
||||
}),
|
||||
0x0001 => IcfData::App(IcfInnerData {
|
||||
id: app_id.clone(),
|
||||
version,
|
||||
datetime,
|
||||
required_system_version,
|
||||
}),
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
0x0002 => {
|
||||
let option_id = String::from_utf8(rd.read_bytes(4)?.to_vec())?;
|
||||
let (datetime, required_system_version) = decode_icf_datetime_version(&mut rd)?;
|
||||
|
||||
for _ in 0..2 {
|
||||
if rd.read_u64()? != 0 {
|
||||
return Err(anyhow!("Padding error. Expected 16 NULL bytes."));
|
||||
}
|
||||
}
|
||||
|
||||
IcfData::Option(IcfOptionData {
|
||||
app_id: app_id.clone(),
|
||||
option_id,
|
||||
datetime,
|
||||
required_system_version,
|
||||
})
|
||||
|
||||
}
|
||||
0x0101 => {
|
||||
let target_version = decode_icf_version(&mut rd)?;
|
||||
let (datetime, required_system_version) = decode_icf_datetime_version(&mut rd)?;
|
||||
|
||||
let source_version = decode_icf_version(&mut rd)?;
|
||||
let (_, _) = decode_icf_datetime_version(&mut rd)?;
|
||||
|
||||
IcfData::Patch(IcfPatchData {
|
||||
id: app_id.clone(),
|
||||
source_version,
|
||||
target_version,
|
||||
required_system_version,
|
||||
datetime,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
println!("Unknown ICF container type {container_type:#06x} at byte {:#06x}, skipping", rd.pos);
|
||||
rd.read_bytes(32)?;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
entries.push(data);
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
pub fn decode_icf(data: &mut [u8]) -> Result<Vec<IcfData>> {
|
||||
let decrypted = decrypt_icf(data, ICF_KEY, ICF_IV)?;
|
||||
|
||||
parse_icf(decrypted)
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
use std::{env, process::exit};
|
||||
|
||||
use anyhow::anyhow;
|
||||
|
||||
use icf::{decode_icf, decrypt_icf, ICF_KEY, ICF_IV, parse_icf, encrypt_icf, fixup_icf};
|
||||
use crate::icf::IcfData;
|
||||
|
||||
mod icf;
|
||||
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() < 2 {
|
||||
println!("Usage: icf-reader.exe <PATH TO ICF>");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if args[1] == "decrypt" {
|
||||
if args.len() < 4 {
|
||||
println!("Usage: icf-reader.exe decrypt <PATH TO ICF> <OUTPUT>");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let mut icf_buf = std::fs::read(args[2].clone())?;
|
||||
let decrypted_icf = decrypt_icf(&mut icf_buf, ICF_KEY, ICF_IV)?;
|
||||
|
||||
std::fs::write(args[3].clone(), decrypted_icf)?;
|
||||
exit(0)
|
||||
}
|
||||
|
||||
if args[1] == "encrypt" {
|
||||
if args.len() < 4 {
|
||||
println!("Usage: icf-reader.exe encrypt <PATH TO ICF> <OUTPUT>");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let mut icf_buf = std::fs::read(args[2].clone())?;
|
||||
fixup_icf(&mut icf_buf)?;
|
||||
let icf = parse_icf(&icf_buf)?;
|
||||
|
||||
for entry in icf {
|
||||
match entry {
|
||||
IcfData::System(data) => println!("{}_{:04}.{:02}.{:02}_{}_0.pack", data.id, data.version.major, data.version.minor, data.version.build, data.datetime.format("%Y%m%d%H%M%S")),
|
||||
IcfData::App(data) => println!("{}_{}.{:02}.{:02}_{}_0.app", data.id, data.version.major, data.version.minor, data.version.build, data.datetime.format("%Y%m%d%H%M%S")),
|
||||
IcfData::Option(data) => println!("{}_{}_{}_0.opt", data.app_id, data.option_id, data.datetime.format("%Y%m%d%H%M%S")),
|
||||
IcfData::Patch(data) => println!("{}_{}.{:02}.{:02}_{}_1_{}.{:02}.{:02}.app", data.id, data.target_version.major, data.target_version.minor, data.target_version.build, data.datetime.format("%Y%m%d%H%M%S"), data.required_system_version.major, data.required_system_version.minor, data.required_system_version.build),
|
||||
}
|
||||
}
|
||||
|
||||
let encrypted_icf = encrypt_icf(&icf_buf, ICF_KEY, ICF_IV)?;
|
||||
|
||||
std::fs::write(args[3].clone(), encrypted_icf)?;
|
||||
|
||||
exit(0)
|
||||
}
|
||||
|
||||
let mut icf_buf = std::fs::read(args[1].clone())?;
|
||||
let icf = decode_icf(&mut icf_buf).map_err(|err| anyhow!("Reading ICF failed: {:#}", err))?;
|
||||
|
||||
for entry in icf {
|
||||
match entry {
|
||||
IcfData::System(data) => println!("{}_{:04}.{:02}.{:02}_{}_0.pack", data.id, data.version.major, data.version.minor, data.version.build, data.datetime.format("%Y%m%d%H%M%S")),
|
||||
IcfData::App(data) => println!("{}_{}.{:02}.{:02}_{}_0.app", data.id, data.version.major, data.version.minor, data.version.build, data.datetime.format("%Y%m%d%H%M%S")),
|
||||
IcfData::Option(data) => println!("{}_{}_{}_0.opt", data.app_id, data.option_id, data.datetime.format("%Y%m%d%H%M%S")),
|
||||
IcfData::Patch(data) => println!("{}_{}.{:02}.{:02}_{}_1_{}.{:02}.{:02}.app", data.id, data.target_version.major, data.target_version.minor, data.target_version.build, data.datetime.format("%Y%m%d%H%M%S"), data.required_system_version.major, data.required_system_version.minor, data.required_system_version.build),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user