From dbbe287b1cc23aee21f76c4fde32660129447ccb Mon Sep 17 00:00:00 2001 From: beerpsi Date: Sun, 21 Jul 2024 23:58:00 +0700 Subject: [PATCH] initial commit --- .gitignore | 9 + Cargo.lock | 276 +++++++++++++++++++++++++++ Cargo.toml | 12 ++ LICENSE | 14 ++ src/bootid.rs | 76 ++++++++ src/crypto.rs | 516 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 193 +++++++++++++++++++ 7 files changed, 1096 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 src/bootid.rs create mode 100644 src/crypto.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65a7303 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/target +*.exe +*.zip +*.app +*.opt +*.vhd +*.ntfs +*.exfat +flamegraph.svg diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1c73144 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,276 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[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 = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[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 = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +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 = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "fsdecrypt" +version = "0.1.0" +dependencies = [ + "aes", + "anyhow", + "cbc", + "crc32fast", + "hex-literal", + "indicatif", +] + +[[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 = "indicatif" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", +] + +[[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 = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "portable-atomic" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1d626e4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "fsdecrypt" +version = "0.1.0" +edition = "2021" + +[dependencies] +aes = "0.8.4" +anyhow = "1.0.86" +cbc = "0.1.2" +crc32fast = "1.4.2" +hex-literal = "0.4.1" +indicatif = "0.17.8" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..458d470 --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ +BSD Zero Clause License + +Copyright (c) 2024 beerpsi + +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. diff --git a/src/bootid.rs b/src/bootid.rs new file mode 100644 index 0000000..cafcccb --- /dev/null +++ b/src/bootid.rs @@ -0,0 +1,76 @@ +use std::fmt::Display; + +use hex_literal::hex; + +pub const BOOTID_KEY: [u8; 16] = hex!("09ca5efd30c9aaef3804d0a7e3fa7120"); +pub const BOOTID_IV: [u8; 16] = hex!("b155c22c2e7f0491fa7f0fdc217aff90"); + +#[allow(non_snake_case)] +pub mod ContainerType { + pub const OS: u16 = 0x0000; + pub const APP: u16 = 0x0101; + pub const OPTION: u16 = 0x0201; +} + +#[derive(Clone, Copy, Debug)] +#[repr(C)] +pub struct Timestamp { + pub year: u16, + pub month: u8, + pub day: u8, + pub hour: u8, + pub minute: u8, + pub second: u8, + unk1: u8, +} + +impl Display for Timestamp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{:<04}{:<02}{:<02}{:<02}{:<02}{:<02}", + self.year, self.month, self.day, self.hour, self.minute, self.second + ) + } +} + +#[derive(Clone, Copy, Debug)] +#[repr(C)] +pub struct Version { + pub release: u8, + pub minor: u8, + pub major: u16, +} + +#[derive(Clone, Copy)] +#[repr(C)] +pub union GameVersion { + pub version: Version, + pub option: [u8; 4], +} + +#[derive(Clone, Copy)] +#[repr(C)] +pub struct BootId { + pub crc32: u32, + pub length: u32, + pub signature: [u8; 4], + pub container_type: u16, + pub sequence_number: u8, + pub use_custom_iv: bool, + pub game_id: [u8; 4], + pub target_timestamp: Timestamp, + pub target_version: GameVersion, + pub block_count: u64, + pub block_size: u64, + pub header_block_count: u64, + unk1: u64, + pub os_id: [u8; 3], + pub os_generation: u8, + pub source_timestamp: Timestamp, + pub source_version: Version, + pub os_version: Version, + pub padding: [u8; 8], + // We don't need the entire bootID, so keep size down by not including the string table. + // pub strings: [u8; 10156], +} diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000..6fb09be --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,516 @@ +use std::path::Path; + +use aes::cipher::{block_padding::NoPadding, BlockDecryptMut, KeyIvInit}; +use anyhow::{anyhow, Result}; +use hex_literal::hex; + +pub const NTFS_HEADER: [u8; 16] = hex!("eb52904e544653202020200010010000"); +pub const EXFAT_HEADER: [u8; 16] = hex!("eb769045584641542020200000000000"); + +pub const OPTION_KEY: [u8; 16] = hex!("5c84a9e726eaa5dd351f2b0750c23697"); +pub const OPTION_IV: [u8; 16] = hex!("c063bf6f562d084d7963c987f5281761"); + +pub type Aes128CbcDec = cbc::Decryptor; + +pub struct GameKeys { + pub key: [u8; 16], + pub iv: Option<[u8; 16]>, +} + +pub fn calculate_page_iv(file_offset: u64, file_iv: &[u8], page_iv: &mut [u8]) { + for (i, (fbyte, pbyte)) in file_iv.iter().zip(page_iv.iter_mut()).enumerate() { + *pbyte = fbyte ^ (file_offset >> (8 * (i % 8))) as u8; + } +} + +pub fn calculate_file_iv( + key: [u8; 16], + expected_header: [u8; 16], + first_page: &[u8], +) -> Result<[u8; 16]> { + let mut iv = [0u8; 16]; + let mut header = [0u8; 16]; + + header.copy_from_slice(&first_page[..16]); + + calculate_page_iv(0, &expected_header, &mut iv); + + let cipher = Aes128CbcDec::new_from_slices(&key, &iv).map_err(|e| anyhow!(e))?; + + cipher + .decrypt_padded_mut::(&mut header) + .map_err(|e| anyhow!(e))?; + + Ok(header) +} + +pub fn get_game_keys(game_id: &str) -> Option { + match game_id { + // Nu Firmware / Hardware Test + "SBZS" => Some(GameKeys { + key: hex!("2ecbcff65ce0abecc10547f8ac8351d8"), + iv: Some(hex!("f2ac6c2817d0574bba113d497e319f3e")), + }), + + // KEY CHIP NU FACTORY / KEY CHIP NUSX FACTORY + "SBZT" => Some(GameKeys { + key: hex!("9ab9ce55ed9c194a715a73a7699f795b"), + iv: Some(hex!("8552de88fedda6e859369fb000f44d5b")), + }), + + // Nu Firmware / Hardware Test + "SBZU" => Some(GameKeys { + key: hex!("eb1228254cdd3077eb3e441c0227bf40"), + iv: Some(hex!("3f9b4676118cee129fe2f1cb2747bca5")), + }), + + // Project DIVA Arcade Future Tone + "SBZV" => Some(GameKeys { + key: hex!("3274a399594d84779625940b69c02d3f"), + iv: Some(hex!("675ba66d29c87923f5f154c406afee42")), + }), + + // Wonderland Wars + "SDAP" => Some(GameKeys { + key: hex!("41b5027c5e99d94aa9335d6d71838ecf"), + iv: Some(hex!("41b5027c5e99d94aa9335d6d71838ecf")), + }), + + // Herobank Arcade + "SDAQ" => Some(GameKeys { + key: hex!("c28f22bc1b339ae64180739886dc83d6"), + iv: Some(hex!("0a29fd145d72bf8dedd436025df0a9fc")), + }), + + // Uranai Collection: Torotte + "SDAV" => Some(GameKeys { + key: hex!("eed95513266a499a55e265b049169c44"), + iv: Some(hex!("84c0e5931d91a6a477d62c271546056e")), + }), + + // Shin Kouchuu Ouja Mushiking + "SDBE" => Some(GameKeys { + key: hex!("7053fb944572e5b631a665cef4b5bcdd"), + iv: Some(hex!("ae4d7e884002c79eb35711554d613057")), + }), + + // E-DEL Sand + "SDBN" => Some(GameKeys { + key: hex!("c1f14ae2e85b095e313c8baec125805e"), + iv: Some(hex!("3c538eea66251acd5404b93f8976a7f7")), + }), + + // CHUNITHM + "SDBT" => Some(GameKeys { + key: hex!("a6a870671fd432ec637adf7a822f97da"), + iv: Some(hex!("2c277f31cd550cfa2c993b4dd56b85ae")), + }), + + // Sonic Dash Extreme + "SDBX" => Some(GameKeys { + key: hex!("3dc19c2d0c20ac199d5fa46e7f6335a6"), + iv: Some(hex!("d8f029ec90fe55be67584f742c55ef8b")), + }), + + // Kancolle Arcade + "SDBZ" => Some(GameKeys { + key: hex!("521bde4460f4184edd879136adeea5ee"), + iv: Some(hex!("1b8324032db69d7b0954794aa229fe68")), + }), + + // crossbeats REV. + "SDCA" => Some(GameKeys { + key: hex!("1649490a03d6c2aec1c496982cb0405c"), + iv: Some(hex!("4680711c7e67a26f9230d5af74b5dcfb")), + }), + + // nailpuri + "SDCD" => Some(GameKeys { + key: hex!("43b38502d8f6d3c7b02b95fc28db5308"), + iv: Some(hex!("6dfcb94bf74f152b55f3e0c7f35b44b5")), + }), + + // Luigi's Mansion Arcade + "SDCF" => Some(GameKeys { + key: hex!("df986883da837538e37b959a3e4117cd"), + iv: Some(hex!("dabf539738852f17714811af70435a83")), + }), + + // Mario & Sonic at Rio Olympic Games + "SDCH" => Some(GameKeys { + key: hex!("e2da769e94f1d3aca1930cdbe0708c9f"), + iv: Some(hex!("c7dcce203c84ab0477236d697570dadc")), + }), + + // KEY CHIP NUSX EDB SOC + "SDCR" => Some(GameKeys { + key: hex!("4961a51fd36f14e72664f52373052160"), + iv: Some(hex!("25d7d1341a282c5e0a34c64562c023ec")), + }), + + // Celevie + "SDCT" => Some(GameKeys { + key: hex!("d6ae51f10ec76da93c981800fc3ad3cb"), + iv: Some(hex!("fb8e43e280d330d06581732f2e11a6dc")), + }), + + // CYTUS Ω + "SDCX" => Some(GameKeys { + key: hex!("79504ccc509b67d1f7a3f593e6f9d9d6"), + iv: Some(hex!("1551ea8926f2aee233eec309de3e5f3c")), + }), + + // KEY CHIP NUSX1.1 TDW + "SDDB" => Some(GameKeys { + key: hex!("875679b2cd1637962b0db25c51fb21a6"), + iv: Some(hex!("8ef44722a0566e8f572356245687fbe5")), + }), + + // Sangokushi Taisen + "SDDD" => Some(GameKeys { + key: hex!("564e967873de6cbcd22efeca6952e9dc"), + iv: Some(hex!("4e3dd465cf09cd82b259f7bed5fc2d6d")), + }), + + // Initial D Arcade Stage Zero + "SDDF" => Some(GameKeys { + key: hex!("65058573a0cb81749e694ae164c61b04"), + iv: Some(hex!("981c4f45e3c6958f054e5d00916bdf2b")), + }), + + // KEY CHIP NU1.1 ESC + "SDDJ" => Some(GameKeys { + key: hex!("630fe52276537bd7fb267adf175f4e99"), + iv: Some(hex!("dc5755be57ded2cdb34433bbba2204ff")), + }), + + // APM3 Sample Program 2 + "SDDL" => Some(GameKeys { + key: hex!("992458295fd06d6a8af0dfb3f6854c19"), + iv: Some(hex!("8484906d4cd5fd225e032843ed37495d")), + }), + + // ALLS MX Factory Dummy + "SDDM" => Some(GameKeys { + key: hex!("0127958210f6ae9bdeb8975018b5af24"), + iv: Some(hex!("181716badccff4bc2b1e29ae02a1bbbb")), + }), + + // Demo ID + "SDDN" => Some(GameKeys { + key: hex!("41dd8e66290117ac67d311a2f0a6416e"), + iv: Some(hex!("73e18e8418f6ceefb11e2767fdea190c")), + }), + + // Soul Reverse + "SDDP" => Some(GameKeys { + key: hex!("cf6d64427eeca47674e17bcd46d1ea8c"), + iv: Some(hex!("ce5174093d26ca2a31b58541e85ac276")), + }), + + // SEGA World Driver Championship + "SDDS" => Some(GameKeys { + key: hex!("161bec6d90989d0e26d791170607a440"), + iv: Some(hex!("81dc26a27028e2092332038aa1bffc47")), + }), + + // O.N.G.E.K.I. + "SDDT" => Some(GameKeys { + key: hex!("3f7658728b9517d3314e684fa2e2a045"), + iv: Some(hex!("41578833c547aaff04db597a6e9eb784")), + }), + + "SDDU" => Some(GameKeys { + key: hex!("649ae9982625f90c55af86713c55d3fd"), + iv: Some(hex!("187116fc4647a7d3b6f2303a34f0a2fe")), + }), + + // ALLS X / X2 Research & Development + "SDDW" => Some(GameKeys { + key: hex!("118565d344f3e14ca69299eeac049bb9"), + iv: Some(hex!("9d6d392ec35ed94ef9fe0a5be0573981")), + }), + + // KEY CHIP ALLS X FACTORY + "SDDX" => Some(GameKeys { + key: hex!("428bff0f9e7aafc169a7a75751ffda98"), + iv: Some(hex!("f8250594f425332c6d349d7ea0e86669")), + }), + + // Shin Kouchuu Ouja Mushiking TWN + "SDEA" => Some(GameKeys { + key: hex!("9f9cf148ac3c50aaf925af1dfb27f58b"), + iv: Some(hex!("4d8ebbd971896b8a4a3dd84a23b329fc")), + }), + + // WCCF FOOTISTA + "SDEB" => Some(GameKeys { + key: hex!("d511ed690415f6359843a134fd47836a"), + iv: Some(hex!("ac139b382acdd112e31564ea7f38186c")), + }), + + // Chrono Regalia + "SDEC" => Some(GameKeys { + key: hex!("f272e5016863af2ba0337f50de686f6e"), + iv: Some(hex!("5327e132631e7f71b61be7cc0df382ce")), + }), + + // CARD MAKER + "SDED" => Some(GameKeys { + key: hex!("21fcec779a16769f5277a36fb542992c"), + iv: Some(hex!("22b50239f1b40ccc3e55a2d69c69b160")), + }), + + // House of the Dead: Scarlet Dawn + "SDEE" => Some(GameKeys { + key: hex!("191eb7440672dab08ddbb7195efb356f"), + iv: Some(hex!("c278b5386dc38bd76d71dbcd826954cf")), + }), + + // FiZ + "SDEG" => Some(GameKeys { + key: hex!("721853dbe2d30bafe24f0edbd210deeb"), + iv: Some(hex!("4dfb0bcec86159aab297166bcd509e6f")), + }), + + // Fate/Grand Order Arcade + "SDEJ" => Some(GameKeys { + key: hex!("9de1ea6ae38d9011f55d8ee864395d24"), + iv: Some(hex!("f60cde21982876d12d17662a48d90836")), + }), + + // ALL.Net P.ras multi Ver.3 + "SDEM" => Some(GameKeys { + key: hex!("700617f293696c07fb9f356d3b99240d"), + iv: Some(hex!("667d026d6cdf329ff351dbaf7098e81d")), + }), + + // StarHorse4 (Server) / MESTA Medal Station + "SDEP" => Some(GameKeys { + key: hex!("fa2b7ca53a823c152d940972cbf532f5"), + iv: Some(hex!("f4af35120c48617704bb5b8471797a62")), + }), + + // Initial D Arcade Stage Zero (CHN) + "SDER" => Some(GameKeys { + key: hex!("7d73367ebb218ec82930d58dc6d7950b"), + iv: Some(hex!("9788c3eca2db6ba92bac4f6f7b706308")), + }), + + // House of the Dead: Scarlet Dawn (EXP) + "SDET" => Some(GameKeys { + key: hex!("4643e7b2c3006e0264163edc8545fb72"), + iv: Some(hex!("612bca81ea2958ffbac36f780f1ed688")), + }), + + // KEY CHIP ALLS X HDZ + "SDEU" => Some(GameKeys { + key: hex!("23b3e9bb47e3ac9998f6e6c1adc4ae33"), + iv: Some(hex!("a964714cea60688407bf554bd1c27ec2")), + }), + + // House of the Dead: Scarlet Dawn (CHN) + "SDEV" => Some(GameKeys { + key: hex!("3c1f018d88926d98163b07a1563a4818"), + iv: Some(hex!("ca7373c9c7dfebac0fc24254c030e4ad")), + }), + + // maimai DX + "SDEZ" => Some(GameKeys { + key: hex!("d136eba05d40e82682e6aad8d9e8688c"), + iv: Some(hex!("c484deeaa0249ef46695f63694b7372f")), + }), + + // KEY CHIP ALLS X REC + "SDFA" => Some(GameKeys { + key: hex!("8e816b4362db24a230877885864d206d"), + iv: Some(hex!("8e5a0ba6a0a1150d47d12bdb64debba7")), + }), + + // WACCA + "SDFE" => Some(GameKeys { + key: hex!("f61719c371e5bca6788c139a53091617"), + iv: Some(hex!("67d43173e343813fa2097fd32992a8e2")), + }), + + // SANDRA + "SDFG" => Some(GameKeys { + key: hex!("3398fb86bfe630a14979411879861ac7"), + iv: Some(hex!("a794c49c2c7639cd80571807c17246ff")), + }), + + // Kemono Friends 3: Planet Tours + "SDFL" => Some(GameKeys { + key: hex!("2449b48067b9176a6e0f9563481e97f4"), + iv: Some(hex!("616f8710454632eb4fb1d89d8c19c19a")), + }), + + // KEY CHIP ALLS X CASJ + "SDFN" => Some(GameKeys { + key: hex!("29f62e22c6a9fd8be327631c68546405"), + iv: Some(hex!("2a860976e6d98513825f291e56cfb5ee")), + }), + + // KEY CHIP NUSX FUTURE + "SDFP" => Some(GameKeys { + key: hex!("570b87263a7ca0aa4c1388e204ee6d4b"), + iv: Some(hex!("7640886011a2300a91fad9f36a8c4775")), + }), + + // StarHorse4 + "SDFT" => Some(GameKeys { + key: hex!("92a25f388c50737e39c3c2f006645f31"), + iv: Some(hex!("a97e72f990417488cb4c67f8f0c3fb25")), + }), + + // Mario & Sonic at TOKYO Olympic + "SDFV" => Some(GameKeys { + key: hex!("fe82db9a60295d829b95f03c2276018b"), + iv: Some(hex!("34d82772ae18174f0a181dc53399ea9c")), + }), + + // maimai DX (EXP) + "SDGA" => Some(GameKeys { + key: hex!("0a6610a62ef670c65b7e7b1750ffb7a1"), + iv: Some(hex!("17a2a22915f81c5896edbba4c412585e")), + }), + + // maimai DX (CHN) + "SDGB" => Some(GameKeys { + key: hex!("7ca4e6b6f3d6e8b26472973887d7fa3a"), + iv: Some(hex!("53fe7135762de3f97e7fe76b0fef3f27")), + }), + + // Puyo Puyo e-Sports Arcade + "SDGH" => Some(GameKeys { + key: hex!("b3e30e7eabac3767ade13c69c9b2f22b"), + iv: Some(hex!("03deaea3742d69675b36cddc8b15ac91")), + }), + + // WCCF FOOTISTA (EXP) + "SDGK" => Some(GameKeys { + key: hex!("9dc4a17fc39fca5a8a358984801caaa7"), + iv: Some(hex!("e0445b11dcfa0dae56c85e8787e11d9b")), + }), + + // KEY CHIP ALLS X HDZ CASJ + "SDGP" => Some(GameKeys { + key: hex!("c87ab31247e7b6ff95fdd79fb91f9f37"), + iv: Some(hex!("2467ab3c031e3dc0568b7077efd27c36")), + }), + + // ROKUMEN + "SDGQ" => Some(GameKeys { + key: hex!("c5356dae7b066bce88984aec36deb62d"), + iv: Some(hex!("4e9f2982460e2fd907bde15709edfba7")), + }), + + // CHUNITHM (EXP) + "SDGS" => Some(GameKeys { + key: hex!("a5150cc5065d2c59ee2f8f332cbd29d5"), + iv: Some(hex!("84014d26696f290ad7ead70c7549bd81")), + }), + + // Initial D THE ARCADE + "SDGT" => Some(GameKeys { + key: hex!("9d0bba20d1e84f2459399f5383beee72"), + iv: Some(hex!("5d340013fdfb2464d253093602fe4b64")), + }), + + "SDGV" => Some(GameKeys { + key: hex!("573f5c8cc44f10f31ec749b695ebe886"), + iv: Some(hex!("6bfca86f9d208a7944cdfc25ea3cd220")), + }), + + // "Eiketsu Taisen: Sanzensekai no Hadou + "SDGY" => Some(GameKeys { + key: hex!("c04b663a59055acbdfebc6d3df0e6a04"), + iv: Some(hex!("76fc5f1d88605107947d0c1ff347022d")), + }), + + // Hori a Tale + "SDGZ" => Some(GameKeys { + key: hex!("9ad74efb208d6ee4fe5ee770331712cf"), + iv: Some(hex!("45f6e53f0eb8fae6665b45444a61e266")), + }), + + // CHUNITHM NEW!! + "SDHD" => Some(GameKeys { + key: hex!("3abd00d7a820ce862eaf474bf6c8f33e"), + iv: Some(hex!("0f1e7eea78da7e037e0552c2843e1b6a")), + }), + + "SDHH" => Some(GameKeys { + key: hex!("fc6f887f3717c5d6713113b92fa3fb27"), + iv: Some(hex!("ad76606460dbe1e91e41bef7ab0c1535")), + }), + + // CHUNITHM (CHN) + "SDHJ" => Some(GameKeys { + key: hex!("985ea66ecb5b1f208c90e2b898f0b073"), + iv: Some(hex!("164a65422e7f01b7f1b0849fc7737cdb")), + }), + + // UFO CATCHER LINK STATION + "SDHK" => Some(GameKeys { + key: hex!("bc92d63c2a099ca2315a483c3041fdd7"), + iv: Some(hex!("b14d8449b6d4325d83a2774b13dd21ff")), + }), + + // WACCA (CHN) + "SDHN" => Some(GameKeys { + key: hex!("892123a26d7c03d49edd12a80ee0c58f"), + iv: Some(hex!("76aa15a6868b8dbdf7207906354d5169")), + }), + + // meityromantic + "SDHR" => Some(GameKeys { + key: hex!("1fb897cab97c8170a6ac0a21685c58d9"), + iv: Some(hex!("f9b60f65b01e8e836a4bc20f7d39faf5")), + }), + + // ALLS System + "ACA" => Some(GameKeys { + key: hex!("e4281bcf48c4d28eb05772ce6f98587a"), + iv: Some(hex!("6cee7f5a2c4b5f1e93c5949114ff0b74")), + }), + + // Read from {game_id}.bin for unknown keys. + _ => { + let filename = format!("{}.bin", game_id); + let path = Path::new(&filename); + + if !path.exists() { + return None; + } + + let Ok(metadata) = path.metadata() else { + return None; + }; + + let size = metadata.len(); + + match size { + 16 => Some(GameKeys { + key: std::fs::read(filename) + .map(|v| v.try_into().unwrap()) + .unwrap(), + iv: None, + }), + 32 => { + let keyiv = std::fs::read(filename).unwrap(); + let key = keyiv[..16].try_into().unwrap(); + let iv: [u8; 16] = keyiv[16..].try_into().unwrap(); + let iv = if iv == NTFS_HEADER || iv == EXFAT_HEADER { + None + } else { + Some(iv) + }; + + Some(GameKeys { key, iv }) + } + _ => None, + } + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..09bf4ff --- /dev/null +++ b/src/main.rs @@ -0,0 +1,193 @@ +use std::{ + fs::File, + io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}, + path::Path, +}; + +use aes::{ + cipher::{block_padding::NoPadding, BlockDecryptMut, InnerIvInit, KeyInit, KeyIvInit}, + Aes128Dec, +}; +use anyhow::{anyhow, Result}; +use bootid::{BootId, ContainerType, BOOTID_IV, BOOTID_KEY}; +use crypto::{ + calculate_file_iv, calculate_page_iv, get_game_keys, Aes128CbcDec, GameKeys, EXFAT_HEADER, + NTFS_HEADER, OPTION_IV, OPTION_KEY, +}; +use indicatif::{ProgressBar, ProgressStyle}; + +mod bootid; +mod crypto; + +const PAGE_SIZE: u64 = 4096; + +fn main() -> Result<()> { + let args = std::env::args().collect::>(); + + if args.len() < 2 { + println!("Usage: fsdecrypt [ ...]"); + return Ok(()); + } + + let bootid_cipher = + Aes128CbcDec::new_from_slices(&BOOTID_KEY, &BOOTID_IV).map_err(|e| anyhow!(e))?; + let mut bootid_bytes = [0u8; std::mem::size_of::()]; + let mut page: Vec = Vec::with_capacity(PAGE_SIZE as usize); + let mut page_iv = [0u8; 16]; + + for path in args.iter().skip(1) { + let path = Path::new(path); + let file = File::open(path)?; + let mut reader = BufReader::with_capacity(0x40000, file); + + reader.read_exact(&mut bootid_bytes)?; + + if let Err(e) = bootid_cipher + .clone() + .decrypt_padded_mut::(&mut bootid_bytes) + { + println!("ERROR: Could not decrypt BootID: {e:#?}"); + continue; + } + + let bootid = unsafe { std::mem::transmute::<[u8; 96], BootId>(bootid_bytes) }; + + if bootid.container_type != ContainerType::OS + && bootid.container_type != ContainerType::APP + && bootid.container_type != ContainerType::OPTION + { + println!("ERROR: Unknown container type {}", bootid.container_type); + continue; + } + + let os_id = std::str::from_utf8(&bootid.os_id)?; + let game_id = std::str::from_utf8(&bootid.game_id)?; + let id = match bootid.container_type { + ContainerType::OS => os_id, + _ => game_id, + }; + + let keys = match bootid.container_type { + ContainerType::OS => get_game_keys(os_id), + ContainerType::APP => get_game_keys(game_id), + _ => Some(GameKeys { + key: OPTION_KEY, + iv: Some(OPTION_IV), + }), + }; + + let Some(keys) = keys else { + println!("ERROR: Key not found for {id}. If you're using a custom key file, ensure the key file is 16/32 bytes and named {id}.bin."); + continue; + }; + + let data_offset = bootid.header_block_count * bootid.block_size; + let key = keys.key; + let iv = if bootid.use_custom_iv { + None + } else { + keys.iv + }; + let iv = match iv { + Some(iv) => iv, + None => { + reader.seek(SeekFrom::Start(data_offset))?; + + let reference = Read::by_ref(&mut reader); + + reference.take(4096).read_to_end(&mut page)?; + + if bootid.container_type == ContainerType::OPTION { + calculate_file_iv(key, EXFAT_HEADER, &page)? + } else { + calculate_file_iv(key, NTFS_HEADER, &page)? + } + } + }; + + let output_filename = match bootid.container_type { + ContainerType::OS => format!( + "{os_id}_{:<04}.{:<02}.{:<02}_{}_{}.ntfs", + bootid.os_version.major, + bootid.os_version.minor, + bootid.os_version.release, + bootid.target_timestamp, + bootid.sequence_number + ), + ContainerType::APP => { + if bootid.sequence_number > 0 { + format!( + "{game_id}_{}.{:<02}.{:<02}_{}_{}_{}.{:<02}.{:<02}.ntfs", + unsafe { bootid.target_version.version.major }, + unsafe { bootid.target_version.version.minor }, + unsafe { bootid.target_version.version.release }, + bootid.target_timestamp, + bootid.sequence_number, + bootid.source_version.major, + bootid.source_version.minor, + bootid.source_version.release, + ) + } else { + format!( + "{game_id}_{}.{:<02}.{:<02}_{}_{}.ntfs", + unsafe { bootid.target_version.version.major }, + unsafe { bootid.target_version.version.minor }, + unsafe { bootid.target_version.version.release }, + bootid.target_timestamp, + bootid.sequence_number, + ) + } + } + _ => format!( + "{game_id}_{}_{}_{}.exfat", + unsafe { std::str::from_utf8(&bootid.target_version.option)? }, + bootid.target_timestamp, + bootid.sequence_number, + ), + }; + let output_path = path.with_file_name(&output_filename); + let output_file = File::create(&output_path)?; + let output_size = (bootid.block_count - bootid.header_block_count) * bootid.block_size; + + output_file.set_len(output_size)?; + + let mut writer = BufWriter::with_capacity(0x40000, output_file); + let cipher = Aes128Dec::new_from_slice(&key).map_err(|e| anyhow!(e))?; + + let pb = ProgressBar::new(output_size) + .with_style( + ProgressStyle::default_bar() + .template("{prefix} [{bar:20!.bright.yellow/dim.white}] {bytes:>8} [{elapsed}<{eta}, {bytes_per_sec}]")? + ); + + pb.set_prefix(output_filename); + reader.seek(SeekFrom::Start(data_offset))?; + + for _ in 0..output_size / PAGE_SIZE { + let file_offset = reader.stream_position()? - data_offset; + let reference = Read::by_ref(&mut reader); + + calculate_page_iv(file_offset, &iv, &mut page_iv); + page.clear(); + reference.take(PAGE_SIZE).read_to_end(&mut page)?; + + let page_cipher = Aes128CbcDec::inner_iv_slice_init(cipher.clone(), &page_iv) + .map_err(|e| anyhow!(e))?; + page_cipher + .decrypt_padded_mut::(&mut page) + .map_err(|e| anyhow!(e))?; + + writer.write_all(&page)?; + pb.inc(PAGE_SIZE); + } + + writer.flush()?; + pb.finish(); + + page.clear(); + page_iv.fill(0); + bootid_bytes.fill(0); + } + + Ok(()) +}