Added UnityCN.

This commit is contained in:
Razmoth
2023-07-10 20:40:17 +04:00
parent 097060be89
commit 3ea9f71f00
18 changed files with 766 additions and 34 deletions

View File

@@ -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>

View File

@@ -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);

View 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})";
}
}
}

View File

@@ -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
View 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"
}
]

View 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}"));
}
}