initial commit

This commit is contained in:
beerpsi 2024-07-21 23:58:00 +07:00
commit dbbe287b1c
7 changed files with 1096 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
/target
*.exe
*.zip
*.app
*.opt
*.vhd
*.ntfs
*.exfat
flamegraph.svg

276
Cargo.lock generated Normal file
View File

@ -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"

12
Cargo.toml Normal file
View File

@ -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"

14
LICENSE Normal file
View File

@ -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.

76
src/bootid.rs Normal file
View File

@ -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],
}

516
src/crypto.rs Normal file
View File

@ -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<aes::Aes128Dec>;
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::<NoPadding>(&mut header)
.map_err(|e| anyhow!(e))?;
Ok(header)
}
pub fn get_game_keys(game_id: &str) -> Option<GameKeys> {
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,
}
}
}
}

193
src/main.rs Normal file
View File

@ -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::<Vec<String>>();
if args.len() < 2 {
println!("Usage: fsdecrypt <input_file1> [<input_file2> ...]");
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::<BootId>()];
let mut page: Vec<u8> = 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::<NoPadding>(&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::<NoPadding>(&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(())
}