diff --git a/AssetStudio/FileReader.cs b/AssetStudio/FileReader.cs index b1c3a68..d27488c 100644 --- a/AssetStudio/FileReader.cs +++ b/AssetStudio/FileReader.cs @@ -169,6 +169,9 @@ namespace AssetStudio case GameType.HelixWaltz2: reader = ParseHelixWaltz2(reader); break; + case GameType.AnchorPanic: + reader = DecryptAnchorPanic(reader); + break; } } if (reader.FileType == FileType.BundleFile && game.Type.IsBlockFile()) diff --git a/AssetStudio/GameManager.cs b/AssetStudio/GameManager.cs index dfa1a7e..f056f27 100644 --- a/AssetStudio/GameManager.cs +++ b/AssetStudio/GameManager.cs @@ -32,6 +32,7 @@ namespace AssetStudio Games.Add(index++, new Game(GameType.ShiningNikki)); Games.Add(index++, new Game(GameType.HelixWaltz2)); Games.Add(index++, new Game(GameType.NetEase)); + Games.Add(index++, new Game(GameType.AnchorPanic)); } public static Game GetGame(GameType gameType) => GetGame((int)gameType); public static Game GetGame(int index) @@ -134,7 +135,8 @@ namespace AssetStudio FantasyOfWind, ShiningNikki, HelixWaltz2, - NetEase + NetEase, + AnchorPanic } public static class GameTypes diff --git a/AssetStudio/ImportHelper.cs b/AssetStudio/ImportHelper.cs index 1aa2498..0a06e4c 100644 --- a/AssetStudio/ImportHelper.cs +++ b/AssetStudio/ImportHelper.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; +using System.Security.Cryptography; using System.Text; using static AssetStudio.BundleFile; using static AssetStudio.Crypto; @@ -421,5 +422,113 @@ namespace AssetStudio return new FileReader(reader.FullPath, ms); } + public static FileReader DecryptAnchorPanic(FileReader reader) + { + const int BlockSize = 0x800; + + var data = reader.ReadBytes(0x1000); + reader.Position = 0; + + var idx = data.Search("UnityFS"); + if (idx != -1) + { + return ParseAlchemyStars(reader); + } + + var key = GetKey(Path.GetFileNameWithoutExtension(reader.FileName)); + + var chunkIndex = 0; + MemoryStream ms = new(); + while (reader.Remaining > 0) + { + var chunkSize = Math.Min((int)reader.Remaining, BlockSize); + var chunk = reader.ReadBytes(chunkSize); + if (IsEncrypt((int)reader.Length, chunkIndex++)) + RC4(chunk, key); + + ms.Write(chunk); + } + + ms.Position = 0; + + return new FileReader(reader.FullPath, ms); + + bool IsEncrypt(int fileSize, int chunkIndex) + { + const int MaxEncryptChunkIndex = 4; + + if (chunkIndex == 0) + return true; + + if (fileSize / BlockSize == chunkIndex) + return true; + + if (MaxEncryptChunkIndex < chunkIndex) + return false; + + return fileSize % 2 == chunkIndex % 2; + } + + byte[] GetKey(string fileName) + { + const string Key = "KxZKZolAT3QXvsUU"; + + string keyHash = CalculateMD5(Key); + string nameHash = CalculateMD5(fileName); + var key = $"{keyHash[..5]}leiyan{nameHash[Math.Max(0, nameHash.Length - 5)..]}"; + return Encoding.UTF8.GetBytes(key); + + string CalculateMD5(string str) + { + var bytes = Encoding.UTF8.GetBytes(str); + bytes = MD5.HashData(bytes); + return Convert.ToHexString(bytes).ToLowerInvariant(); + } + } + + void RC4(Span data, byte[] key) + { + int[] S = new int[0x100]; + for (int _ = 0; _ < 0x100; _++) + { + S[_] = _; + } + + int[] T = new int[0x100]; + + if (key.Length == 0x100) + { + Buffer.BlockCopy(key, 0, T, 0, key.Length); + } + else + { + for (int _ = 0; _ < 0x100; _++) + { + T[_] = key[_ % key.Length]; + } + } + + int i = 0; + int j = 0; + for (i = 0; i < 0x100; i++) + { + j = (j + S[i] + T[i]) % 0x100; + + (S[j], S[i]) = (S[i], S[j]); + } + + i = j = 0; + for (int iteration = 0; iteration < data.Length; iteration++) + { + i = (i + 1) % 0x100; + j = (j + S[i]) % 0x100; + + (S[j], S[i]) = (S[i], S[j]); + var K = (uint)S[(S[j] + S[i]) % 0x100]; + + data[iteration] ^= Convert.ToByte(K); + } + } + } } }