diff --git a/Cargo.lock b/Cargo.lock index 6942941..b3a06ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,10 +14,81 @@ dependencies = [ ] [[package]] -name = "anyhow" -version = "1.0.86" +name = "android_system_properties" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "autocfg" @@ -26,10 +97,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "bitflags" -version = "2.9.1" +name = "binrw" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "ab81d22cbd2d745852348b2138f3db2103afa8ce043117a374581926a523e267" +dependencies = [ + "array-init", + "binrw_derive", + "bytemuck", +] + +[[package]] +name = "binrw_derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b019a3efebe7f453612083202887b6f1ace59e20d010672e336eea4ed5be97" +dependencies = [ + "either", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" dependencies = [ "bytemuck", ] @@ -44,25 +138,37 @@ dependencies = [ ] [[package]] -name = "bytemuck" -version = "1.23.1" +name = "bumpalo" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytemuck" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.9.3" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" +checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] +[[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" @@ -73,10 +179,20 @@ dependencies = [ ] [[package]] -name = "cfg-if" -version = "1.0.0" +name = "cc" +version = "1.2.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "checked_num" @@ -87,6 +203,19 @@ dependencies = [ "num-traits", ] +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "cipher" version = "0.4.4" @@ -98,32 +227,90 @@ dependencies = [ ] [[package]] -name = "console" -version = "0.15.8" +name = "clap" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ - "encode_unicode", - "lazy_static", - "libc", - "unicode-width", - "windows-sys", + "clap_builder", + "clap_derive", ] [[package]] -name = "cpufeatures" -version = "0.2.12" +name = "clap_builder" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "console" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.61.1", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -159,7 +346,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.106", ] [[package]] @@ -170,7 +357,16 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.106", +] + +[[package]] +name = "deranged" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +dependencies = [ + "powerfmt", ] [[package]] @@ -191,7 +387,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -201,14 +397,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn", + "syn 2.0.106", ] [[package]] -name = "encode_unicode" -version = "0.3.6" +name = "derive_more" +version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.106", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "endify" @@ -227,7 +453,7 @@ checksum = "e7a204d4aa704d0a74ee3bebccc65f24bca0cdef2588f08eaafda467cd4d3c8a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -238,7 +464,18 @@ checksum = "1f2f9bd649bd8c884aa518a798861fd111699dfc7d7dcc193bc2b658c8b9188d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", +] + +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -256,6 +493,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" + [[package]] name = "fnv" version = "1.0.7" @@ -264,15 +507,20 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "fsdecrypt" -version = "0.1.1" +version = "0.1.7" dependencies = [ "aes", "anyhow", "cbc", + "chrono", + "cipher", + "clap", "crc32fast", "exfat-fs", "hex-literal", "indicatif", + "ntfs", + "thiserror", ] [[package]] @@ -286,10 +534,46 @@ dependencies = [ ] [[package]] -name = "hex-literal" +name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex-literal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcaaec4551594c969335c98c903c1397853d4198408ea609190f420500f6be71" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "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 = "ident_case" @@ -299,47 +583,98 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indicatif" -version = "0.17.8" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" dependencies = [ "console", - "instant", - "number_prefix", "portable-atomic", "unicode-width", + "unit-prefix", + "web-time", ] [[package]] name = "inout" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "block-padding", "generic-array", ] [[package]] -name = "instant" -version = "0.1.13" +name = "is_terminal_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ - "cfg-if", + "once_cell", + "wasm-bindgen", ] [[package]] -name = "lazy_static" -version = "1.5.0" +name = "libc" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] -name = "libc" -version = "0.2.155" +name = "log" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nt-string" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64f73b19d9405e886b53b9dee286e7fbb622a5276a7fd143c2d8e4dac3a0c6c" +dependencies = [ + "displaydoc", + "widestring", +] + +[[package]] +name = "ntfs" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fe2140f3b6eef2648fa331ca7af7652da1bf3bb84f715878bf50f2f8f03c38" +dependencies = [ + "arrayvec", + "binrw", + "bitflags", + "byteorder", + "derive_more", + "displaydoc", + "enumn", + "memoffset", + "nt-string", + "strum_macros", + "time", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" @@ -351,22 +686,34 @@ dependencies = [ ] [[package]] -name = "number_prefix" -version = "0.4.0" +name = "once_cell" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "portable-atomic" -version = "1.7.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -380,6 +727,62 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "strsim" version = "0.11.1" @@ -387,10 +790,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "syn" -version = "2.0.104" +name = "strum_macros" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -399,63 +826,249 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", ] [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + +[[package]] +name = "unit-prefix" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "widestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" + +[[package]] +name = "windows-core" +version = "0.62.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-interface" +version = "0.59.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link", +] [[package]] name = "windows-sys" -version = "0.52.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets", ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "windows-sys" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +dependencies = [ + "windows-link", "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", @@ -468,48 +1081,48 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[package]] name = "windows_aarch64_msvc" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" [[package]] name = "windows_i686_gnullvm" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[package]] name = "windows_i686_msvc" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] name = "windows_x86_64_msvc" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" diff --git a/Cargo.toml b/Cargo.toml index f8966b5..afc83a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,23 @@ [package] name = "fsdecrypt" -version = "0.1.1" +version = "0.1.7" edition = "2021" [dependencies] aes = "0.8.4" -anyhow = "1.0.86" +anyhow = "1.0.100" cbc = "0.1.2" -crc32fast = "1.4.2" -hex-literal = "0.4.1" -indicatif = "0.17.8" +crc32fast = "1.5.0" +hex-literal = "1.0.0" +indicatif = "0.18.0" exfat-fs = "0.1.3" +chrono = "0.4.42" +clap = { version = "4.5.48", features = ["derive"] } +ntfs = "0.4.0" +thiserror = "2.0.18" +cipher = { version = "0.4.4", features = ["alloc"] } + +[profile.release] +lto = "thin" +codegen-units = 1 +strip = "none" diff --git a/src/bootid.rs b/src/bootid.rs index 24e7733..132eea0 100644 --- a/src/bootid.rs +++ b/src/bootid.rs @@ -1,4 +1,4 @@ -use std::fmt::Display; +use std::fmt::{Debug, Display}; use hex_literal::hex; @@ -75,3 +75,30 @@ pub struct BootId { // We don't need the entire bootID, so keep size down by not including the string table. // pub strings: [u8; 10156], } + +impl Debug for BootId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BootId") + .field("crc32", &self.crc32) + .field("length", &self.length) + .field("signature", &self.signature) + .field("unk1", &self.unk1) + .field("container_type", &self.container_type) + .field("sequence_number", &self.sequence_number) + .field("use_custom_iv", &self.use_custom_iv) + .field("game_id", &self.game_id) + .field("target_timestamp", &self.target_timestamp) + //.field("target_version", &self.target_version) + .field("block_count", &self.block_count) + .field("block_size", &self.block_size) + .field("header_block_count", &self.header_block_count) + .field("unk2", &self.unk2) + .field("os_id", &self.os_id) + .field("os_generation", &self.os_generation) + .field("source_timestamp", &self.source_timestamp) + .field("source_version", &self.source_version) + .field("os_version", &self.os_version) + .field("padding", &self.padding) + .finish() + } +} diff --git a/src/crypto.rs b/src/crypto.rs index 6fb09be..6e282d8 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -12,6 +12,7 @@ pub const OPTION_IV: [u8; 16] = hex!("c063bf6f562d084d7963c987f5281761"); pub type Aes128CbcDec = cbc::Decryptor; +#[derive(Debug, Clone, Copy)] pub struct GameKeys { pub key: [u8; 16], pub iv: Option<[u8; 16]>, diff --git a/src/main.rs b/src/main.rs index 62381a9..fded235 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,58 +1,123 @@ use std::{ - fs::{create_dir_all, File}, - io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}, - path::Path, + fs::{create_dir_all, File, FileTimes}, + io::{BufRead, BufReader, BufWriter, Write}, + path::{Path, PathBuf}, + time::{Duration, SystemTime}, }; +use chrono::{FixedOffset, TimeZone}; +use clap::Parser; use exfat_fs::dir::{entry::fs::FsElement, Root}; -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 bootid::ContainerType; use indicatif::{ProgressBar, ProgressStyle}; +use ntfs::{ + indexes::NtfsFileNameIndex, structured_values::NtfsStandardInformation, Ntfs, + NtfsAttributeType, NtfsTime, +}; + +use crate::stream::FscryptDecryptor; mod bootid; mod crypto; +mod stream; -const PAGE_SIZE: u64 = 4096; +fn exfat_timestamp_to_system_time( + timestamp: &exfat_fs::timestamp::Timestamp, +) -> Result { + let exfat_date = timestamp.date(); + let exfat_time = timestamp.time(); + // exFAT UTC offset is in 15-minute intervals, so 1 = UTC+00:15, 2 = UTC+00:30, etc. + let exfat_utc_offset = timestamp.utc_offset() as i32 * 15 * 60; + let chrono_date_time = FixedOffset::east_opt(exfat_utc_offset) + .ok_or_else(|| anyhow!("invaid utc offset: {}", timestamp.utc_offset()))? + .with_ymd_and_hms( + exfat_date.year as i32, + exfat_date.month as u32, + exfat_date.day as u32, + exfat_time.hour as u32, + exfat_time.minute as u32, + exfat_time.second as u32, + ) + .unwrap(); + + return Ok(SystemTime::UNIX_EPOCH + + Duration::from_micros(chrono_date_time.timestamp_micros().try_into()?)); +} fn extract_exfat_contents(exfat_path: &Path) -> Result<()> { - println!("Extracting contents of {}", exfat_path.display()); - - let file = File::open(exfat_path)?; - let mut root = Root::open(file)?; - // Create output directory with same name as exfat file (without extension) let output_dir = exfat_path.with_extension(""); + let file = FscryptDecryptor::new(File::open(exfat_path)?).map_err(|e| anyhow!(e))?; + let mut root = Root::open(file)?; + + let pb = ProgressBar::new(calculate_exfat_size(root.items())?) + .with_style( + ProgressStyle::default_bar().template( + "{prefix} [{bar:20!.bright.yellow/dim.white}] {bytes:>8} [{elapsed}<{eta}, {bytes_per_sec}]", + )? + ); + pb.set_prefix(format!( + "Extracting {}", + exfat_path.file_name().unwrap().display(), + )); + create_dir_all(&output_dir)?; - extract_fs_elements(root.items(), &output_dir)?; + extract_exfat_elements(root.items(), &output_dir, &pb)?; + + pb.finish(); Ok(()) } -fn extract_fs_elements(elements: &mut [FsElement], output_dir: &Path) -> Result<()> { +fn calculate_exfat_size(elements: &[FsElement>]) -> Result { + let mut total = 0; + + for element in elements { + match element { + FsElement::F(ref file) => total += file.len(), + FsElement::D(directory) => total += calculate_exfat_size(&directory.open()?)?, + } + } + + Ok(total) +} + +fn extract_exfat_elements( + elements: &mut [FsElement>], + output_dir: &Path, + pb: &ProgressBar, +) -> Result<()> { for element in elements { match element { FsElement::F(ref mut file) => { let dest_path = output_dir.join(file.name()); let mut dest = File::create(dest_path)?; - std::io::copy(file, &mut dest)?; + dest.set_times( + FileTimes::new() + .set_accessed(exfat_timestamp_to_system_time( + file.timestamps().accessed(), + )?) + .set_modified(exfat_timestamp_to_system_time( + file.timestamps().modified(), + )?), + )?; + + let mut writer = BufWriter::with_capacity(256 * 1024, &mut dest); + + std::io::copy(file, &mut writer)?; + writer.flush()?; + pb.inc(file.len()); } FsElement::D(directory) => { let dest_path = output_dir.join(directory.name()); create_dir_all(&dest_path)?; let mut children = directory.open()?; - extract_fs_elements(&mut children, &dest_path)?; + extract_exfat_elements(&mut children, &dest_path, &pb)?; } } } @@ -60,182 +125,161 @@ fn extract_fs_elements(elements: &mut [FsElement], output_dir: &Path) -> R Ok(()) } +fn ntfs_time_to_system_time(ntfs_time: NtfsTime) -> SystemTime { + // An NTFS "interval" is 100 nanoseconds. + // The Windows epoch is 1601-01-01, while the Unix epoch is 1970-01-01. + let intervals_since_windows_epoch = ntfs_time.nt_timestamp(); + let intervals_since_unix_epoch = intervals_since_windows_epoch - 116_444_736_000_000_000; + let nanos_since_unix_epoch = intervals_since_unix_epoch * 100; + + return SystemTime::UNIX_EPOCH + Duration::from_nanos(nanos_since_unix_epoch); +} + +fn extract_internal_vhd(image_path: &Path, sequence_number: u8) -> Result { + let vhd_filename = format!("internal_{sequence_number}.vhd"); + let output_path = image_path.with_extension("vhd"); + + let mut fs = FscryptDecryptor::new(File::open(image_path)?).map_err(|e| anyhow!(e))?; + + let mut ntfs = Ntfs::new(&mut fs)?; + + ntfs.read_upcase_table(&mut fs)?; + + let root_directory = ntfs.root_directory(&mut fs)?; + let index = root_directory.directory_index(&mut fs)?; + let mut finder = index.finder(); + let entry = NtfsFileNameIndex::find(&mut finder, &ntfs, &mut fs, &vhd_filename) + .ok_or_else(|| anyhow!("could not find VHD {vhd_filename}"))??; + let file = entry.to_file(&ntfs, &mut fs)?; + let data_item = file + .data(&mut fs, "") + .ok_or_else(|| anyhow!("file data does not exist"))??; + let data_attribute = data_item.to_attribute()?; + let mut data_value = + BufReader::with_capacity(256 * 1024, data_attribute.value(&mut fs)?.attach(&mut fs)); + + let mut output_file = File::create(&output_path)?; + + let pb = ProgressBar::new(data_attribute.value_length() as u64) + .with_style( + ProgressStyle::default_bar() + .template("{prefix} [{bar:20!.bright.yellow/dim.white}] {bytes:>8} [{elapsed}<{eta}, {bytes_per_sec}]")? + ); + pb.set_prefix(format!("{}", output_path.file_name().unwrap().display())); + + loop { + let buffer = data_value.fill_buf()?; + let length = buffer.len(); + + if length == 0 { + break; + } + + output_file.write_all(buffer)?; + data_value.consume(length); + pb.inc(length as u64); + } + output_file.flush()?; + + pb.finish(); + + let mut attributes_iterator = file.attributes(); + + while let Some(attribute) = attributes_iterator.next(&mut fs) { + let attribute = attribute?; + let attribute = attribute.to_attribute()?; + + match attribute.ty() { + Ok(NtfsAttributeType::StandardInformation) => { + let info = attribute.resident_structured_value::()?; + + output_file.set_times( + FileTimes::new() + .set_accessed(ntfs_time_to_system_time(info.access_time())) + .set_modified(ntfs_time_to_system_time(info.modification_time())), + )?; + + break; + } + _ => continue, + } + } + + Ok(output_path) +} + +#[derive(Parser)] +#[command(version, about = "decryptor for some SEGA containers", long_about = None)] +struct Cli { + #[arg(long, help = "do not extract contents of decrypted image")] + no_extract: bool, + + #[arg(required = true)] + files: Vec, +} + fn main() -> Result<()> { - let args = std::env::args().collect::>(); + let cli = Cli::parse(); - 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, - ), - }; + for path in &cli.files { + let file = FscryptDecryptor::new(File::open(path)?).map_err(|e| anyhow!(e))?; + let bootid = file.bootid.clone(); + let output_filename = file.filename()?; 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)?; + // Can't directly extract options since we can't impl exfat_fs::dir::ReadOffset + // for our wrapper decryptor + if cli.no_extract { + let mut output_file = File::create(&output_path)?; - let mut writer = BufWriter::with_capacity(0x40000, output_file); - let cipher = Aes128Dec::new_from_slice(&key).map_err(|e| anyhow!(e))?; + output_file.set_len(file.len())?; - 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}]")? - ); + let pb = ProgressBar::new(file.len()) + .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.clone()); - reader.seek(SeekFrom::Start(data_offset))?; + pb.set_prefix(output_filename.clone()); - for _ in 0..output_size / PAGE_SIZE { - let file_offset = reader.stream_position()? - data_offset; - let reference = Read::by_ref(&mut reader); + let mut reader = BufReader::with_capacity(0x40000, file); - calculate_page_iv(file_offset, &iv, &mut page_iv); - page.clear(); - reference.take(PAGE_SIZE).read_to_end(&mut page)?; + loop { + let buffer = reader.fill_buf()?; + let length = buffer.len(); - 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))?; + if length == 0 { + break; + } - writer.write_all(&page)?; - pb.inc(PAGE_SIZE); - } + output_file.write_all(&buffer)?; + reader.consume(length); - writer.flush()?; - pb.finish(); + pb.inc(length as u64); + } - // Extract exfat contents if this is an exfat file - if bootid.container_type == ContainerType::OPTION - && output_path.extension().unwrap_or_default() == "exfat" - { - if let Err(e) = extract_exfat_contents(&output_path) { - println!("WARNING: Failed to extract exfat contents: {e:#?}"); - } else { - println!("Extracted exfat contents: {:?}", output_path); - println!("Deleting exfat file: {:?}", output_path); - - std::fs::remove_file(output_path)?; + output_file.flush()?; + } else { + match bootid.container_type { + ContainerType::OS | ContainerType::APP => { + match extract_internal_vhd(&path, bootid.sequence_number) { + Ok(_) => {} + Err(e) => { + println!("WARNING: Failed to extract internal VHD: {e:#?}"); + } + } + } + ContainerType::OPTION => match extract_exfat_contents(&path) { + Ok(_) => {} + Err(e) => { + println!("WARNING: Failed to extract exfat contents: {e:#?}"); + } + }, + _ => { + println!("WARNING: Unknown container type: {}", bootid.container_type); + } } } - - page.clear(); - page_iv.fill(0); - bootid_bytes.fill(0); } Ok(()) diff --git a/src/stream.rs b/src/stream.rs new file mode 100644 index 0000000..13e94c6 --- /dev/null +++ b/src/stream.rs @@ -0,0 +1,328 @@ +use std::{ + fs::File, + io::{self, Read, Seek, SeekFrom}, + str::Utf8Error, +}; + +use aes::cipher::{ + block_padding::{NoPadding, UnpadError}, + BlockDecryptMut, InvalidLength, +}; +use cipher::KeyIvInit; +use exfat_fs::disk::ReadOffset; + +use crate::{ + bootid::{BootId, ContainerType, BOOTID_IV, BOOTID_KEY}, + crypto::{ + calculate_file_iv, calculate_page_iv, get_game_keys, Aes128CbcDec, GameKeys, EXFAT_HEADER, + NTFS_HEADER, OPTION_IV, OPTION_KEY, + }, +}; + +const PAGE_SIZE: u64 = 4096; + +#[derive(Debug, thiserror::Error)] +pub enum DecryptError { + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error(transparent)] + Utf8(#[from] std::str::Utf8Error), + + #[error(transparent)] + Unknown(#[from] anyhow::Error), + + #[error("Invalid IV length: {0:?}")] + AesInvalidLength(InvalidLength), + + #[error("Malformed padding: {0:?}")] + AesUnpadError(UnpadError), + + #[error("Unknown container type {0}")] + InvalidContainerType(u8), + + #[error("There were no decryption keys for this container.")] + NoMatchingKeys, +} + +/// Decryptor for an fscrypt container. +#[derive(Debug)] +pub struct FscryptDecryptor { + /// The fscrypt file. + input: R, + + /// The fscrypt file's bootid. + pub bootid: BootId, + + key: [u8; 16], + iv: [u8; 16], + + encrypted_page: Vec, + plaintext_pos: u64, + page: Option>, +} + +impl FscryptDecryptor { + pub fn len(&self) -> u64 { + self.bootid.block_size * (self.bootid.block_count - self.bootid.header_block_count) + } + + pub fn filename(&self) -> Result { + let bootid = self.bootid; + let os_id = std::str::from_utf8(&bootid.os_id)?; + let game_id = std::str::from_utf8(&bootid.game_id)?; + + let 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, + ), + }; + + Ok(filename) + } +} + +impl FscryptDecryptor { + pub fn new(mut input: R) -> Result { + let bootid_cipher = Aes128CbcDec::new_from_slices(&BOOTID_KEY, &BOOTID_IV) + .map_err(|e| DecryptError::AesInvalidLength(e))?; + let mut bootid_bytes = [0u8; std::mem::size_of::()]; + + input.read_exact(&mut bootid_bytes)?; + bootid_cipher + .decrypt_padded_mut::(&mut bootid_bytes) + .map_err(|e| DecryptError::AesUnpadError(e))?; + + let bootid: BootId = unsafe { std::mem::transmute(bootid_bytes) }; + + if bootid.container_type != ContainerType::OS + && bootid.container_type != ContainerType::APP + && bootid.container_type != ContainerType::OPTION + { + return Err(DecryptError::InvalidContainerType(bootid.container_type)); + } + + let data_offset = bootid.header_block_count * bootid.block_size; + let mut page: Vec = Vec::with_capacity(PAGE_SIZE as usize); + let keys = match bootid.container_type { + ContainerType::OS => get_game_keys(std::str::from_utf8(&bootid.os_id)?), + ContainerType::APP => get_game_keys(std::str::from_utf8(&bootid.game_id)?), + _ => Some(GameKeys { + key: OPTION_KEY, + iv: Some(OPTION_IV), + }), + }; + let Some(keys) = keys else { + return Err(DecryptError::NoMatchingKeys); + }; + let iv = if bootid.use_custom_iv { None } else { keys.iv }; + let iv = match iv { + Some(iv) => iv, + None => { + input.seek(SeekFrom::Start(data_offset))?; + + let reference = Read::by_ref(&mut input); + + reference.take(PAGE_SIZE).read_to_end(&mut page)?; + + if bootid.container_type == ContainerType::OPTION { + calculate_file_iv(keys.key, EXFAT_HEADER, &page)? + } else { + calculate_file_iv(keys.key, NTFS_HEADER, &page)? + } + } + }; + + input.seek(SeekFrom::Start(data_offset))?; + + Ok(Self { + input, + bootid, + key: keys.key, + iv, + encrypted_page: Vec::with_capacity(PAGE_SIZE as usize), + plaintext_pos: 0, + page: None, + }) + } + + fn decrypt_page(&mut self) -> io::Result<()> { + let page = &self.encrypted_page; + let file_offset = self.input.stream_position()? + - (self.bootid.header_block_count * self.bootid.block_size) + - PAGE_SIZE; + let mut page_iv = [0u8; 16]; + + calculate_page_iv(file_offset, &self.iv, &mut page_iv); + + let page_cipher = Aes128CbcDec::new_from_slices(&self.key, &page_iv).unwrap(); + + self.page = Some( + page_cipher + .decrypt_padded_vec_mut::(&page) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "failed to decrypt"))?, + ); + self.encrypted_page.clear(); + + Ok(()) + } + + fn read_from_page(&mut self, buf: &mut [u8]) -> usize { + let Some(ref page) = self.page else { + return 0; + }; + + let page_offset = (self.plaintext_pos % PAGE_SIZE) as usize; + let to_read = std::cmp::min(page.len() - page_offset, buf.len()); + + buf[..to_read].copy_from_slice(&page[page_offset..page_offset + to_read]); + + self.plaintext_pos += to_read as u64; + + if self.plaintext_pos % PAGE_SIZE == 0 { + self.page = None; + } + + to_read + } +} + +impl Read for FscryptDecryptor { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + if self.page.is_none() { + if (self.encrypted_page.len() as u64) < PAGE_SIZE { + Read::by_ref(&mut self.input) + .take(PAGE_SIZE) + .read_to_end(&mut self.encrypted_page)?; + } + + self.decrypt_page()?; + } + + Ok(self.read_from_page(buf)) + } +} + +impl Seek for FscryptDecryptor { + fn seek(&mut self, pos: SeekFrom) -> io::Result { + let start = self.bootid.header_block_count * self.bootid.block_size; + let target_pos = match pos { + SeekFrom::Start(offset) => offset, + SeekFrom::Current(offset) => { + let res = (self.plaintext_pos as i64) + offset; + + if res < 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "cannot seek before the start", + )); + } + + res as u64 + } + SeekFrom::End(offset) => { + let length = (self.bootid.block_count - self.bootid.header_block_count) + * self.bootid.block_size; + let res = (length as i64) + offset; + + if res < 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "cannot seek before the start", + )); + } + + res as u64 + } + }; + let page_index = self.plaintext_pos / PAGE_SIZE; + let target_index = target_pos / PAGE_SIZE; + let target_offset = target_pos % PAGE_SIZE; + + if page_index == target_index { + self.plaintext_pos = target_pos; + return Ok(target_pos); + } + + self.page = None; + self.input + .seek(SeekFrom::Start(start + target_index * PAGE_SIZE))?; + self.plaintext_pos = target_index * PAGE_SIZE; + + if target_offset > 0 { + let mut to_drop = vec![0; target_offset as usize]; + self.read_exact(&mut to_drop)?; + } + + Ok(target_pos) + } +} + +impl ReadOffset for FscryptDecryptor { + type Err = std::io::Error; + + fn read_at(&self, offset: u64, buffer: &mut [u8]) -> Result { + let start = self.bootid.header_block_count * self.bootid.block_size; + let target_index = offset / PAGE_SIZE; + let target_offset = offset % PAGE_SIZE; + let mut page = [0u8; 4096]; + let mut page_iv = [0u8; 16]; + let page_offset = target_index * PAGE_SIZE; + + #[cfg(unix)] + std::os::unix::fs::FileExt::read_at(&self.input, &mut page, start + page_offset)?; + + #[cfg(windows)] + std::os::windows::fs::FileExt::seek_read(&self.input, &mut page, start + page_offset)?; + + calculate_page_iv(page_offset, &self.iv, &mut page_iv); + + let page_cipher = Aes128CbcDec::new_from_slices(&self.key, &page_iv).unwrap(); + + page_cipher + .decrypt_padded_mut::(&mut page) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "failed to decrypt"))?; + + let to_read = std::cmp::min(buffer.len(), (PAGE_SIZE - target_offset) as usize); + + buffer[..to_read] + .copy_from_slice(&page[target_offset as usize..target_offset as usize + to_read]); + + Ok(to_read) + } +}