diff --git a/Cargo.lock b/Cargo.lock index 6b804f3..5211df1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1573,6 +1573,7 @@ dependencies = [ "serde_yaml_ng", "sha1", "sha2", + "siphasher", "stylua", "tendril", "unicode-segmentation", diff --git a/Cargo.toml b/Cargo.toml index bb46143..50a33c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ serde_json = "1" serde_yaml_ng = "0.10" sha1 = { version = "0.11", optional = true } sha2 = { version = "0.11", optional = true } +siphasher = { version = "1.0", optional = true } stylua = { version = "2.1", optional = true, default-features = false} tendril = { version = "0.5", optional = true } unicode-segmentation = "1.12" @@ -101,7 +102,7 @@ hexen-haus = ["memchr", "utils-str"] hexen-haus-arc = ["hexen-haus"] hexen-haus-img = ["hexen-haus", "image"] kirikiri = ["emote-psb", "fancy-regex", "flate2", "json", "lz4", "utils-escape"] -kirikiri-arc = ["kirikiri", "adler", "aes", "base64", "blake2", "bytes", "cbc", "chacha20/legacy", "fastcdc", "flate2", "hex", "int-enum", "md5", "msg_tool_macro/kirikiri-arc", "msg_tool_xp3data", "parse-size", "sha2", "utils-case-insensitive-string", "utils-lzss", "utils-serde-base64bytes", "utils-simple-pack", "zopfli", "zstd"] +kirikiri-arc = ["kirikiri", "adler", "aes", "base64", "blake2", "bytes", "cbc", "chacha20/legacy", "fastcdc", "flate2", "hex", "int-enum", "md5", "msg_tool_macro/kirikiri-arc", "msg_tool_xp3data", "parse-size", "sha2", "siphasher", "utils-case-insensitive-string", "utils-lzss", "utils-serde-base64bytes", "utils-simple-pack", "zopfli", "zstd"] kirikiri-img = ["kirikiri", "image", "libtlg-rs"] musica = [] musica-arc = ["musica", "crc32fast", "flate2", "include-flate", "utils-blowfish", "utils-rc4", "utils-serde-base64bytes", "utils-xored-stream"] diff --git a/msg_tool_xp3data/crypt.json b/msg_tool_xp3data/crypt.json index c14a495..63d07a3 100644 --- a/msg_tool_xp3data/crypt.json +++ b/msg_tool_xp3data/crypt.json @@ -151,6 +151,20 @@ "TpmFileName": "plugin/AmairoIsleNauts.tpm", "Title": "天色*アイルノーツ | 天色幻想岛" }, + "AMBITIOUS MISSION After Episode 1": { + "$type": "HxCrypt", + "Mask": 454, + "Offset": 656, + "PrologOrder": "AAEC", + "OddBranchOrder": "BAMCBQAB", + "EvenBranchOrder": "AAIHAwQBBgU=", + "Key": "Wa7phHMk/a0hEztfLDpfcWbN1n7e0DYitcdCV1m/2EQ=", + "Nonce": "DDEouJ4BezmeMVKZf7WVqA==", + "FilterKey": 375708990042069900, + "RandomType": 1, + "ControlBlockName": "amb_after_ep1.bin", + "Title": "AMBITIOUS MISSION アフターエピソード1 かぐや&あてな | Ambitious Mission FD1" + }, "Anata no Milk Kudasai na": { "$type": "HashCrypt", "Title": "あなたの精液くださいな ~私とボクとどっちがお好み?~" @@ -310,6 +324,67 @@ "ControlBlockName": "cafe_stella.bin", "Title": "喫茶ステラと死神の蝶 | 星光咖啡馆与死神之蝶 | 星光咖啡館與死神之蝶" }, + "Café Stella to Shinigami no Chou [Steam]": { + "$type": "HxCrypt", + "Mask": 486, + "Offset": 101, + "PrologOrder": "AgAB", + "OddBranchOrder": "BAEFAAMC", + "EvenBranchOrder": "BgMBAgQFAAc=", + "FilterKey": 9413765695581088110, + "IndexKeyDict": { + "data.xp3": { + "Key": "sd8wA9myXvbA4ATJnq6JYia3V5otV84MkuSiW4aYOog=", + "Nonce": "HXJsNtEkHsOPDBcx/QODdQ==" + }, + "video.xp3": { + "Key": "sd8wA9myXvbA4ATJnq6JYia3V5otV84MkuSiW4aYOog=", + "Nonce": "HXJsNtEkHsOPDBcx/QODdQ==" + }, + "bgm.xp3": { + "Key": "sd8wA9myXvbA4ATJnq6JYia3V5otV84MkuSiW4aYOog=", + "Nonce": "HXJsNtEkHsOPDBcx/QODdQ==" + }, + "fgimage.xp3": { + "Key": "sd8wA9myXvbA4ATJnq6JYia3V5otV84MkuSiW4aYOog=", + "Nonce": "HXJsNtEkHsOPDBcx/QODdQ==" + }, + "bgimage.xp3": { + "Key": "sd8wA9myXvbA4ATJnq6JYia3V5otV84MkuSiW4aYOog=", + "Nonce": "HXJsNtEkHsOPDBcx/QODdQ==" + }, + "scn.xp3": { + "Key": "sd8wA9myXvbA4ATJnq6JYia3V5otV84MkuSiW4aYOog=", + "Nonce": "HXJsNtEkHsOPDBcx/QODdQ==" + }, + "voice.xp3": { + "Key": "sd8wA9myXvbA4ATJnq6JYia3V5otV84MkuSiW4aYOog=", + "Nonce": "HXJsNtEkHsOPDBcx/QODdQ==" + }, + "main.xp3": { + "Key": "sd8wA9myXvbA4ATJnq6JYia3V5otV84MkuSiW4aYOog=", + "Nonce": "HXJsNtEkHsOPDBcx/QODdQ==" + }, + "evimage.xp3": { + "Key": "sd8wA9myXvbA4ATJnq6JYia3V5otV84MkuSiW4aYOog=", + "Nonce": "HXJsNtEkHsOPDBcx/QODdQ==" + }, + "uipsd.xp3": { + "Key": "sd8wA9myXvbA4ATJnq6JYia3V5otV84MkuSiW4aYOog=", + "Nonce": "HXJsNtEkHsOPDBcx/QODdQ==" + }, + "video2.xp3": { + "Key": "9lU4Kgw+IvRM3Zm0ubsa161RAxURD9ghXmSvYH7f/VE=", + "Nonce": "r5eGhOPufNjqgZMwtggGhg==" + }, + "adult.xp3": { + "Key": "9lU4Kgw+IvRM3Zm0ubsa161RAxURD9ghXmSvYH7f/VE=", + "Nonce": "r5eGhOPufNjqgZMwtggGhg==" + } + }, + "ControlBlockName": "cafe_stella_steam.bin", + "Title": "喫茶ステラと死神の蝶 [Steam] | 星光咖啡馆与死神之蝶 [Steam] | 星光咖啡館與死神之蝶 [Steam]" + }, "CharaBration!": { "$type": "AkabeiCrypt", "Seed": 3854429322, @@ -420,7 +495,8 @@ "Key": "4ozUw00nStHvknH8ECpqL1BVC9nOrpdryY4wHKtBTFE=", "Nonce": "pjhv8KmiW/gnbdoy6c5J6Q==", "FilterKey": 12271333071625965214, - "ControlBlockName": "dc5.bin" + "ControlBlockName": "dc5.bin", + "Title": "D.C.5 ~ダ・カーポ5~ | 初音岛5 | D.C.5 Plus Happiness ~ダ・カーポ5~プラスハピネス | D.C.5 ~Da Capo 5~ Future Link | D.C.5 Future Link ~ダ・カーポ5~ フューチャーリンク | D.C.5 Sweet Happiness ~ダ・カーポ5~スイートハピネス" }, "Deatte 5-fun wa Ore no Mono!": { "$type": "XorCrypt", diff --git a/msg_tool_xp3data/cx_cb/amb_after_ep1.bin b/msg_tool_xp3data/cx_cb/amb_after_ep1.bin new file mode 100644 index 0000000..ccdd0a7 Binary files /dev/null and b/msg_tool_xp3data/cx_cb/amb_after_ep1.bin differ diff --git a/msg_tool_xp3data/cx_cb/cafe_stella_steam.bin b/msg_tool_xp3data/cx_cb/cafe_stella_steam.bin new file mode 100644 index 0000000..5a5aedd Binary files /dev/null and b/msg_tool_xp3data/cx_cb/cafe_stella_steam.bin differ diff --git a/src/scripts/kirikiri/archive/xp3/crypt/cx.rs b/src/scripts/kirikiri/archive/xp3/crypt/cx.rs index 5e940c5..0d6b947 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/cx.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/cx.rs @@ -2111,7 +2111,7 @@ impl HxCrypt { index_key = Some(ind.clone()) } let index_key = index_key.ok_or_else(|| anyhow::anyhow!("Can not find index key."))?; - let (file_map, path_map) = if let Some(path) = file_list_path { + let (file_map, mut path_map) = if let Some(path) = file_list_path { let data = std::fs::read(path)?; let data = decode_to_string(Encoding::Utf8, &data, true)?; Self::read_names(&data, s)? @@ -2128,6 +2128,10 @@ impl HxCrypt { (HashMap::new(), HashMap::new()) } }; + let default_path_hash = calculate_path_hash("", "xp3hnp"); + if !path_map.contains_key(&default_path_hash) { + path_map.insert(default_path_hash, String::new()); + } Ok(Self { base: CxEncryption::new_inner( base, @@ -2952,6 +2956,18 @@ fn create_garbage_filename_set(file_hash_salt: &str) -> HashSet { set } +fn calculate_path_hash(pathname: &str, path_hash_salt: &str) -> PathHash { + use std::hash::Hasher; + let mut hasher = siphasher::sip::SipHasher24::new(); + (pathname.to_lowercase() + path_hash_salt) + .encode_utf16() + .for_each(|b| { + hasher.write(&b.to_le_bytes()); + }); + let data = hasher.finish().to_ne_bytes(); + PathHash(u64::from_be_bytes(data)) +} + #[test] fn test_filehash_deserialize() { assert_eq!( @@ -2965,3 +2981,15 @@ fn test_filehash_deserialize() { .unwrap() ); } + +#[test] +fn test_calculate_path_hash() { + assert_eq!( + calculate_path_hash("", "xp3hnp"), + PathHash::try_from("94d4a97c61498621").unwrap(), + ); + assert_eq!( + calculate_path_hash("scenario/scripts/", "xp3hnp"), + PathHash::try_from("c81c19411c1a5e54").unwrap(), + ); +}