Added UnityCN.
This commit is contained in:
@@ -15,5 +15,11 @@
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="ZstdSharp.Port" Version="0.7.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Keys.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace AssetStudio
|
||||
BlocksInfoAtTheEnd = 0x80,
|
||||
OldWebPluginCompatibility = 0x100,
|
||||
BlockInfoNeedPaddingAtStart = 0x200,
|
||||
UnityCNEncryption = 0x400
|
||||
}
|
||||
|
||||
[Flags]
|
||||
@@ -66,6 +67,7 @@ namespace AssetStudio
|
||||
}
|
||||
|
||||
private Game Game;
|
||||
private UnityCN UnityCN;
|
||||
|
||||
public Header m_Header;
|
||||
private Node[] m_DirectoryInfo;
|
||||
@@ -75,6 +77,7 @@ namespace AssetStudio
|
||||
|
||||
|
||||
private bool HasUncompressedDataHash = true;
|
||||
private bool HasBlockInfoNeedPaddingAtStart = true;
|
||||
|
||||
public BundleFile(FileReader reader, Game game)
|
||||
{
|
||||
@@ -100,6 +103,10 @@ namespace AssetStudio
|
||||
case "UnityFS":
|
||||
case "ENCR":
|
||||
ReadHeader(reader);
|
||||
if (game.Type.IsUnityCN())
|
||||
{
|
||||
ReadUnityCN(reader);
|
||||
}
|
||||
ReadBlocksInfoAndDirectory(reader);
|
||||
using (var blocksStream = CreateBlocksStream(reader.FullPath))
|
||||
{
|
||||
@@ -312,6 +319,32 @@ namespace AssetStudio
|
||||
}
|
||||
}
|
||||
|
||||
private void ReadUnityCN(EndianBinaryReader reader)
|
||||
{
|
||||
ArchiveFlags mask;
|
||||
|
||||
var version = ParseVersion();
|
||||
//Flag changed it in these versions
|
||||
if (version[0] < 2020 || //2020 and earlier
|
||||
(version[0] == 2020 && version[1] == 3 && version[2] <= 34) || //2020.3.34 and earlier
|
||||
(version[0] == 2021 && version[1] == 3 && version[2] <= 2) || //2021.3.2 and earlier
|
||||
(version[0] == 2022 && version[1] == 3 && version[2] <= 1)) //2022.3.1 and earlier
|
||||
{
|
||||
mask = ArchiveFlags.BlockInfoNeedPaddingAtStart;
|
||||
HasBlockInfoNeedPaddingAtStart = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
mask = ArchiveFlags.UnityCNEncryption;
|
||||
HasBlockInfoNeedPaddingAtStart = true;
|
||||
}
|
||||
|
||||
if ((m_Header.flags & mask) != 0)
|
||||
{
|
||||
UnityCN = new UnityCN(reader);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReadBlocksInfoAndDirectory(FileReader reader)
|
||||
{
|
||||
byte[] blocksInfoBytes;
|
||||
@@ -403,7 +436,7 @@ namespace AssetStudio
|
||||
};
|
||||
}
|
||||
}
|
||||
if ((m_Header.flags & ArchiveFlags.BlockInfoNeedPaddingAtStart) != 0)
|
||||
if (HasBlockInfoNeedPaddingAtStart && (m_Header.flags & ArchiveFlags.BlockInfoNeedPaddingAtStart) != 0)
|
||||
{
|
||||
reader.AlignStream(16);
|
||||
}
|
||||
@@ -439,14 +472,18 @@ namespace AssetStudio
|
||||
{
|
||||
compressedBytesSpan = Mr0kUtils.Decrypt(compressedBytesSpan, (Mr0k)Game);
|
||||
}
|
||||
if (Game.Type.IsOPFP())
|
||||
if (Game.Type.IsUnityCN() && ((int)blockInfo.flags & 0x100) != 0)
|
||||
{
|
||||
OPFPUtils.Decrypt(compressedBytesSpan, reader.FullPath);
|
||||
UnityCN.DecryptBlock(compressedBytes, compressedSize, i);
|
||||
}
|
||||
if (Game.Type.IsNetEase() && i == 0)
|
||||
{
|
||||
NetEaseUtils.Decrypt(compressedBytesSpan);
|
||||
}
|
||||
if (Game.Type.IsOPFP())
|
||||
{
|
||||
OPFPUtils.Decrypt(compressedBytesSpan, reader.FullPath);
|
||||
}
|
||||
var uncompressedSize = (int)blockInfo.uncompressedSize;
|
||||
var uncompressedBytes = BigArrayPool<byte>.Shared.Rent(uncompressedSize);
|
||||
var uncompressedBytesSpan = uncompressedBytes.AsSpan(0, uncompressedSize);
|
||||
|
||||
158
AssetStudio/Crypto/UnityCN.cs
Normal file
158
AssetStudio/Crypto/UnityCN.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace AssetStudio
|
||||
{
|
||||
public class UnityCN
|
||||
{
|
||||
private const string Signature = "#$unity3dchina!@";
|
||||
|
||||
private static ICryptoTransform Encryptor;
|
||||
|
||||
public byte[] Index = new byte[0x10];
|
||||
public byte[] Sub = new byte[0x10];
|
||||
|
||||
public UnityCN(EndianBinaryReader reader)
|
||||
{
|
||||
reader.ReadUInt32();
|
||||
|
||||
var infoBytes = reader.ReadBytes(0x10);
|
||||
var infoKey = reader.ReadBytes(0x10);
|
||||
reader.Position += 1;
|
||||
|
||||
var signatureBytes = reader.ReadBytes(0x10);
|
||||
var signatureKey = reader.ReadBytes(0x10);
|
||||
reader.Position += 1;
|
||||
|
||||
DecryptKey(signatureKey, signatureBytes);
|
||||
|
||||
var str = Encoding.UTF8.GetString(signatureBytes);
|
||||
if (str != Signature)
|
||||
throw new Exception("Invalid Signature !!");
|
||||
|
||||
DecryptKey(infoKey, infoBytes);
|
||||
|
||||
infoBytes = infoBytes.ToUInt4Array();
|
||||
infoBytes.AsSpan(0, 0x10).CopyTo(Index);
|
||||
var subBytes = infoBytes.AsSpan(0x10, 0x10);
|
||||
for (var i = 0; i < subBytes.Length; i++)
|
||||
{
|
||||
var idx = (i % 4 * 4) + (i / 4);
|
||||
Sub[idx] = subBytes[i];
|
||||
}
|
||||
}
|
||||
|
||||
public static bool SetKey(Entry entry)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var aes = Aes.Create())
|
||||
{
|
||||
aes.Mode = CipherMode.ECB;
|
||||
aes.Key = Convert.FromHexString(entry.Key);
|
||||
|
||||
Encryptor = aes.CreateEncryptor();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error($"[UnityCN] Invalid key !!\n{e.Message}");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void DecryptBlock(Span<byte> bytes, int size, int index)
|
||||
{
|
||||
var offset = 0;
|
||||
while (offset < size)
|
||||
{
|
||||
offset += Decrypt(bytes.Slice(offset), index++, size - offset);
|
||||
}
|
||||
}
|
||||
|
||||
private void DecryptKey(byte[] key, byte[] data)
|
||||
{
|
||||
if (Encryptor != null)
|
||||
{
|
||||
key = Encryptor.TransformFinalBlock(key, 0, key.Length);
|
||||
for (int i = 0; i < 0x10; i++)
|
||||
data[i] ^= key[i];
|
||||
}
|
||||
}
|
||||
|
||||
private int DecryptByte(Span<byte> bytes, ref int offset, ref int index)
|
||||
{
|
||||
var b = Sub[((index >> 2) & 3) + 4] + Sub[index & 3] + Sub[((index >> 4) & 3) + 8] + Sub[((byte)index >> 6) + 12];
|
||||
bytes[offset] = (byte)((Index[bytes[offset] & 0xF] - b) & 0xF | 0x10 * (Index[bytes[offset] >> 4] - b));
|
||||
b = bytes[offset];
|
||||
offset++;
|
||||
index++;
|
||||
return b;
|
||||
}
|
||||
|
||||
private int Decrypt(Span<byte> bytes, int index, int remaining)
|
||||
{
|
||||
var offset = 0;
|
||||
|
||||
var curByte = DecryptByte(bytes, ref offset, ref index);
|
||||
var byteHigh = curByte >> 4;
|
||||
var byteLow = curByte & 0xF;
|
||||
|
||||
if (byteHigh == 0xF)
|
||||
{
|
||||
int b;
|
||||
do
|
||||
{
|
||||
b = DecryptByte(bytes, ref offset, ref index);
|
||||
byteHigh += b;
|
||||
} while (b == 0xFF);
|
||||
}
|
||||
|
||||
offset += byteHigh;
|
||||
|
||||
if (offset < remaining)
|
||||
{
|
||||
DecryptByte(bytes, ref offset, ref index);
|
||||
DecryptByte(bytes, ref offset, ref index);
|
||||
if (byteLow == 0xF)
|
||||
{
|
||||
int b;
|
||||
do
|
||||
{
|
||||
b = DecryptByte(bytes, ref offset, ref index);
|
||||
} while (b == 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
public class Entry
|
||||
{
|
||||
public string Name { get; private set; }
|
||||
public string Key { get; private set; }
|
||||
|
||||
public Entry(string name, string key)
|
||||
{
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
public bool Validate()
|
||||
{
|
||||
var bytes = Convert.FromHexString(Key);
|
||||
if (bytes.Length != 0x10)
|
||||
{
|
||||
Logger.Warning($"[UnityCN] {this} has invalid key, size should be 16 bytes, skipping...");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Name} ({Key})";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ namespace AssetStudio
|
||||
{
|
||||
int index = 0;
|
||||
Games.Add(index++, new(GameType.Normal));
|
||||
Games.Add(index++, new(GameType.UnityCN));
|
||||
Games.Add(index++, new Mhy0(GameType.GI, GIMhy0ShiftRow, GIMhy0Key, GIMhy0Mul, GIExpansionKey, GISBox, GIInitVector, GIInitSeed));
|
||||
Games.Add(index++, new Mr0k(GameType.GI_Pack, PackExpansionKey, blockKey: PackBlockKey));
|
||||
Games.Add(index++, new Mr0k(GameType.GI_CB1));
|
||||
@@ -116,6 +117,7 @@ namespace AssetStudio
|
||||
public enum GameType
|
||||
{
|
||||
Normal,
|
||||
UnityCN,
|
||||
GI,
|
||||
GI_Pack,
|
||||
GI_CB1,
|
||||
@@ -142,6 +144,7 @@ namespace AssetStudio
|
||||
public static class GameTypes
|
||||
{
|
||||
public static bool IsNormal(this GameType type) => type == GameType.Normal;
|
||||
public static bool IsUnityCN(this GameType type) => type == GameType.UnityCN;
|
||||
public static bool IsGI(this GameType type) => type == GameType.GI;
|
||||
public static bool IsGIPack(this GameType type) => type == GameType.GI_Pack;
|
||||
public static bool IsGICB1(this GameType type) => type == GameType.GI_CB1;
|
||||
|
||||
86
AssetStudio/Keys.json
Normal file
86
AssetStudio/Keys.json
Normal file
@@ -0,0 +1,86 @@
|
||||
[
|
||||
{
|
||||
"Name": "PGR GLB/KR",
|
||||
"Key": "6B75726F6B75726F6B75726F6B75726F"
|
||||
},
|
||||
{
|
||||
"Name": "PGR CN/JP/TW",
|
||||
"Key": "7935585076714C4F72436F6B57524961"
|
||||
},
|
||||
{
|
||||
"Name": "Archeland/Kalpa of Universe",
|
||||
"Key": "426C61636B4A61636B50726F6A656374"
|
||||
},
|
||||
{
|
||||
"Name": "Archeland 1.1.14",
|
||||
"Key": "50726F6A65637441726368654C616E64"
|
||||
},
|
||||
{
|
||||
"Name": "Neural Cloud",
|
||||
"Key": "31636162383436663532393031633965"
|
||||
},
|
||||
{
|
||||
"Name": "Higan: Eruthyll",
|
||||
"Key": "45317832633361346C35693662377572"
|
||||
},
|
||||
{
|
||||
"Name": "White Chord",
|
||||
"Key": "79756C6F6E6731383638676E6F6C7579"
|
||||
},
|
||||
{
|
||||
"Name": "Mecharashi",
|
||||
"Key": "33384338334631333245374637413041"
|
||||
},
|
||||
{
|
||||
"Name": "Castlevania: Moon Night Fantasy",
|
||||
"Key": "31323334353637383132333435363738"
|
||||
},
|
||||
{
|
||||
"Name": "Huā Yì Shān Xīn Zhī Yuè",
|
||||
"Key": "494E484A6E68647970716B3534377864"
|
||||
},
|
||||
{
|
||||
"Name": "Doula Continent",
|
||||
"Key": "52346366773339474644326661785756"
|
||||
},
|
||||
{
|
||||
"Name": "Bless Global",
|
||||
"Key": "6C6F6E67747567616D652E796A66623F"
|
||||
},
|
||||
{
|
||||
"Name": "Starside",
|
||||
"Key": "41394A3542384D4A50554D3539464B57"
|
||||
},
|
||||
{
|
||||
"Name": "Resonance Soltice",
|
||||
"Key": "5265736F6E616E63655265626F726E52"
|
||||
},
|
||||
{
|
||||
"Name": "Oblivion Override",
|
||||
"Key": "7179666D6F6F6E323331323433343532"
|
||||
},
|
||||
{
|
||||
"Name": "Dawnlands",
|
||||
"Key": "636F6465737339353237636F64657373"
|
||||
},
|
||||
{
|
||||
"Name": "BB",
|
||||
"Key": "5F6C4E3F3A3F233F3F3F3F663F1A3F3F"
|
||||
},
|
||||
{
|
||||
"Name": "Dynasty Legends 2",
|
||||
"Key": "746169686567616D6573323032323032"
|
||||
},
|
||||
{
|
||||
"Name": "Evernight CN",
|
||||
"Key": "68687878747478736868787874747873"
|
||||
},
|
||||
{
|
||||
"Name": "Xintianlong Babu",
|
||||
"Key": "61323562623133346363326464333265"
|
||||
},
|
||||
{
|
||||
"Name": "Frostpunk: Beyond the Ice",
|
||||
"Key": "7368756978696E673838383838383838"
|
||||
}
|
||||
]
|
||||
74
AssetStudio/UnityCNManager.cs
Normal file
74
AssetStudio/UnityCNManager.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AssetStudio
|
||||
{
|
||||
public static class UnityCNManager
|
||||
{
|
||||
public const string KeysFileName = "Keys.json";
|
||||
|
||||
private static List<UnityCN.Entry> Entries = new List<UnityCN.Entry>();
|
||||
|
||||
static UnityCNManager()
|
||||
{
|
||||
var str = File.ReadAllText(KeysFileName);
|
||||
Entries = JsonConvert.DeserializeObject<List<UnityCN.Entry>>(str);
|
||||
}
|
||||
|
||||
public static void SaveEntries(List<UnityCN.Entry> entries)
|
||||
{
|
||||
Entries.Clear();
|
||||
Entries.AddRange(entries);
|
||||
|
||||
var str = JsonConvert.SerializeObject(Entries);
|
||||
File.WriteAllText(KeysFileName, str);
|
||||
}
|
||||
|
||||
public static void SetKey(int index)
|
||||
{
|
||||
if (TryGetEntry(index, out var unityCN))
|
||||
{
|
||||
if (UnityCN.SetKey(unityCN))
|
||||
{
|
||||
Logger.Info($"[UnityCN] Selected Key is {unityCN}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Info($"[UnityCN] No Key is selected !!");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error("Invalid Key !!");
|
||||
Logger.Warning(GetEntries().Select(x => x.ToString()).ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryGetEntry(int index, out UnityCN.Entry key)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (index < 0 || index > Entries.Count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
key = Entries[index];
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Logger.Error($"[UnityCN] Invalid Index, check if list is not empty !!\n{e.Message}");
|
||||
key = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
public static UnityCN.Entry[] GetEntries() => Entries.ToArray();
|
||||
|
||||
public new static string ToString() => string.Join("\n", GetEntries().Select((x, i) => $"{i}: {x.Name}"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user