Initial commit

This commit is contained in:
beerpsi 2024-01-02 14:02:55 +07:00
commit a0808a7081
7 changed files with 892 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

418
Cargo.lock generated Normal file
View File

@ -0,0 +1,418 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aes"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anyhow"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "binary-reader"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d173c51941d642588ed6a13d464617e3a9176b8fe00dc2de182434c36812a5e"
dependencies = [
"byteorder",
]
[[package]]
name = "block-padding"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cbc"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
dependencies = [
"cipher",
]
[[package]]
name = "cc"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"libc",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "cpufeatures"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "hex-literal"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
[[package]]
name = "iana-time-zone"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icf-reader"
version = "0.1.0"
dependencies = [
"aes",
"anyhow",
"binary-reader",
"cbc",
"chrono",
"crc32fast",
"hex-literal",
]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"block-padding",
"generic-array",
]
[[package]]
name = "js-sys"
version = "0.3.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4"
[[package]]
name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "num-traits"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "proc-macro2"
version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "2.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasm-bindgen"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
[[package]]
name = "windows-core"
version = "0.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"

15
Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "icf-reader"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
aes = "0.8.3"
anyhow = "1.0.75"
binary-reader = "0.4.5"
cbc = { version = "0.1.2", features = ["std"] }
chrono = "0.4.31"
crc32fast = "1.3.2"
hex-literal = "0.4.1"

5
LICENSE Normal file
View File

@ -0,0 +1,5 @@
Copyright (C) 2023 by beerpsi beerpsi@noreply.gitea.tendokyu.moe
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

19
README.md Normal file
View File

@ -0,0 +1,19 @@
# icf-reader
Tools for decoding, decrypting and encrypting ICFs
## Usage
```shell
icf-reader <PATH TO ICF>
icf-reader <decrypt | encrypt> <INPUT> <OUTPUT>
```
Encrypting the ICF will also correct CRC checksums, so worrying about the
correct checksum is not needed.
## Building
```
ICF_KEY=<key> ICF_IV=<iv> cargo build --release
ls target/release/icf-reader.exe
```

365
src/icf.rs Normal file
View File

@ -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
src/main.rs Normal file
View File

@ -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(())
}