diff --git a/AssetStudio/BundleFile.cs b/AssetStudio/BundleFile.cs index 9ad68c4..34d8904 100644 --- a/AssetStudio/BundleFile.cs +++ b/AssetStudio/BundleFile.cs @@ -3,10 +3,9 @@ using System; using System.Data; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using System.Text; +using System.Text.RegularExpressions; using System.Collections.Generic; -using System.Runtime.InteropServices; namespace AssetStudio { @@ -110,8 +109,7 @@ namespace AssetStudio private List m_BlocksInfo; public List fileList; - - + private bool HasUncompressedDataHash = true; private bool HasBlockInfoNeedPaddingAtStart = true; @@ -530,7 +528,15 @@ namespace AssetStudio } case CompressionType.Lzma: //LZMA { - SevenZipHelper.StreamDecompress(reader.BaseStream, blocksStream, blockInfo.compressedSize, blockInfo.uncompressedSize); + var compressedStream = reader.BaseStream; + if (Game.Type.IsNetEase() && i == 0) + { + var compressedBytesSpan = reader.ReadBytes((int)blockInfo.compressedSize).AsSpan(); + NetEaseUtils.DecryptWithoutHeader(compressedBytesSpan); + var ms = new MemoryStream(compressedBytesSpan.ToArray()); + compressedStream = ms; + } + SevenZipHelper.StreamDecompress(compressedStream, blocksStream, blockInfo.compressedSize, blockInfo.uncompressedSize); break; } case CompressionType.Lz4: //LZ4 @@ -553,7 +559,7 @@ namespace AssetStudio } if (Game.Type.IsNetEase() && i == 0) { - NetEaseUtils.Decrypt(compressedBytesSpan); + NetEaseUtils.DecryptWithHeader(compressedBytesSpan); } if (Game.Type.IsArknightsEndfield() && i == 0) { diff --git a/AssetStudio/Crypto/NetEaseUtils.cs b/AssetStudio/Crypto/NetEaseUtils.cs index 0b7079c..044016d 100644 --- a/AssetStudio/Crypto/NetEaseUtils.cs +++ b/AssetStudio/Crypto/NetEaseUtils.cs @@ -9,29 +9,38 @@ namespace AssetStudio public static class NetEaseUtils { private static readonly byte[] Signature = new byte[] { 0xEE, 0xDD }; - public static void Decrypt(Span bytes) + public static void DecryptWithHeader(Span bytes) + { + var (encryptedOffset, encryptedSize) = ReadHeader(bytes); + var encrypted = bytes.Slice(encryptedOffset, encryptedSize); + Decrypt(encrypted); + } + public static void DecryptWithoutHeader(Span bytes) + { + var encrypted = bytes[..Math.Min(bytes.Length, 0x1000)]; + Decrypt(encrypted); + } + private static void Decrypt(Span bytes) { Logger.Verbose($"Attempting to decrypt block with NetEase encryption..."); - var (encryptedOffset, encryptedSize) = ReadHeader(bytes); - var encrypted = bytes.Slice(encryptedOffset, encryptedSize); - var encryptedInts = MemoryMarshal.Cast(encrypted); + var encryptedInts = MemoryMarshal.Cast(bytes); - var seedInts = new int[] { encryptedInts[3], encryptedInts[1], encryptedInts[4], encrypted.Length, encryptedInts[2] }; + var seedInts = new int[] { encryptedInts[3], encryptedInts[1], encryptedInts[4], bytes.Length, encryptedInts[2] }; var seedBytes = MemoryMarshal.AsBytes(seedInts).ToArray(); var seed = (int)CRC.CalculateDigest(seedBytes, 0, (uint)seedBytes.Length); var keyPart0 = seed ^ (encryptedInts[7] + 0x1981); - var keyPart1 = seed ^ (encrypted.Length + 0x2013); + var keyPart1 = seed ^ (bytes.Length + 0x2013); var keyPart2 = seed ^ (encryptedInts[5] + 0x1985); var keyPart3 = seed ^ (encryptedInts[6] + 0x2018); for (int i = 0; i < 0x20; i++) { - encrypted[i] ^= 0xA6; + bytes[i] ^= 0xA6; } - var block = encrypted[0x20..]; + var block = bytes[0x20..]; var keyVector = new int[] { keyPart2, keyPart0, keyPart1, keyPart3 }; var keysVector = new int[] { 0x571, keyPart3, 0x892, 0x750, keyPart2, keyPart0, 0x746, keyPart1, 0x568 }; if (block.Length >= 0x80) @@ -65,7 +74,7 @@ namespace AssetStudio var remainingCount = dataBlock.Length % 0x80; if (remainingCount > 0) { - var remaining = encrypted[^remainingCount..]; + var remaining = bytes[^remainingCount..]; for (int i = 0; i < remainingCount; i++) { remaining[i] ^= (byte)(keyBlock[i] ^ ((uint)keysVector[(uint)keyVector[i % keyVector.Length] % keysVector.Length] % 0xFF) ^ i); diff --git a/AssetStudio/FileReader.cs b/AssetStudio/FileReader.cs index 2b4ebcd..4b4f63e 100644 --- a/AssetStudio/FileReader.cs +++ b/AssetStudio/FileReader.cs @@ -212,6 +212,9 @@ namespace AssetStudio case GameType.Reverse1999: reader = DecryptReverse1999(reader); break; + case GameType.JJKPhantomParade: + reader = DecryptReverse1999(reader); + break; } } if (reader.FileType == FileType.BundleFile && game.Type.IsBlockFile() || reader.FileType == FileType.ENCRFile || reader.FileType == FileType.BlbFile) diff --git a/AssetStudio/GameManager.cs b/AssetStudio/GameManager.cs index 2376f6b..3f2fbcd 100644 --- a/AssetStudio/GameManager.cs +++ b/AssetStudio/GameManager.cs @@ -43,6 +43,7 @@ namespace AssetStudio Games.Add(index++, new Game(GameType.GirlsFrontline)); Games.Add(index++, new Game(GameType.Reverse1999)); Games.Add(index++, new Game(GameType.ArknightsEndfield)); + Games.Add(index++, new Game(GameType.JJKPhantomParade)); } public static Game GetGame(GameType gameType) => GetGame((int)gameType); public static Game GetGame(int index) @@ -156,7 +157,9 @@ namespace AssetStudio CodenameJump, GirlsFrontline, Reverse1999, - ArknightsEndfield + ArknightsEndfield, + JJKPhantomParade, + } public static class GameTypes diff --git a/AssetStudio/ImportHelper.cs b/AssetStudio/ImportHelper.cs index f0fac76..f82ab71 100644 --- a/AssetStudio/ImportHelper.cs +++ b/AssetStudio/ImportHelper.cs @@ -950,5 +950,71 @@ namespace AssetStudio return (byte)(key + (byte)(2 * ((key & 1) + 1))); } } + + public static FileReader DecryptJJKPhantomParade(FileReader reader) + { + Logger.Verbose($"Attempting to decrypt file {reader.FileName} with Jujutsu Kaisen: Phantom Parade encryption"); + + var key = reader.ReadBytes(2); + var signatureBytes = reader.ReadBytes(13); + var generation = reader.ReadByte(); + + for (int i = 0; i < 13; i++) + { + signatureBytes[i] ^= key[i % key.Length]; + } + + var signature = Encoding.UTF8.GetString(signatureBytes); + if (signature != "_GhostAssets_") + { + throw new Exception("Invalid signature"); + } + + generation ^= (byte)(key[0] ^ key[1]); + + if (generation != 1) + { + throw new Exception("Invalid generation"); + } + + + long value = 0; + var data = reader.ReadBytes((int)reader.Remaining); + var blockCount = data.Length / 0x10; + + using var writerMS = new MemoryStream(); + using var writer = new BinaryWriter(writerMS); + for (int i = 0; i <= blockCount; i++) + { + if (i % 0x40 == 0) + { + value = 0x64 * ((i / 0x40) + 1); + } + writer.Write(value); + writer.Write((long)0); + value += 1; + } + + using var aes = Aes.Create(); + aes.Key = new byte[] { 0x36, 0x31, 0x35, 0x34, 0x65, 0x30, 0x30, 0x66, 0x39, 0x45, 0x39, 0x63, 0x65, 0x34, 0x36, 0x64, 0x63, 0x39, 0x30, 0x35, 0x34, 0x45, 0x30, 0x37, 0x31, 0x37, 0x33, 0x41, 0x61, 0x35, 0x34, 0x36 }; + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.None; + var encryptor = aes.CreateEncryptor(); + + var keyBytes = writerMS.ToArray(); + keyBytes = encryptor.TransformFinalBlock(keyBytes, 0, keyBytes.Length); + + for (int i = 0; i < data.Length; i++) + { + data[i] ^= keyBytes[i]; + } + + Logger.Verbose("Decrypted Jujutsu Kaisen: Phantom Parade file successfully !!"); + + MemoryStream ms = new(); + ms.Write(data); + ms.Position = 0; + return new FileReader(reader.FullPath, ms); + } } }