Skip to content

Commit 6e42e52

Browse files
committed
Wider encryption support
1 parent 13919d4 commit 6e42e52

File tree

6 files changed

+231
-18
lines changed

6 files changed

+231
-18
lines changed

TACTSharp/BLTE.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ namespace TACTSharp
66
{
77
public static class BLTE
88
{
9+
private static Dictionary<string, byte[]> ArmadilloKeys = new();
10+
911
public static byte[] Decode(ReadOnlySpan<byte> data, ulong totalDecompSize = 0, bool verify = false)
1012
{
1113
var fixedHeaderSize = 8;
@@ -152,5 +154,32 @@ private static bool TryDecrypt(ReadOnlySpan<byte> data, int chunkIndex, out Span
152154
throw new Exception("encType arc4 not implemented");
153155
}
154156
}
157+
158+
public static bool TryDecryptArmadillo(string name, string keyName, ReadOnlySpan<byte> data, out Span<byte> output, int offset = 0)
159+
{
160+
if (!KeyService.TryGetArmadilloKey(keyName, out var key))
161+
{
162+
if (!File.Exists(keyName + ".ak"))
163+
{
164+
key = new byte[16];
165+
throw new Exception("Armadillo key " + keyName + " not set and not found on disk (" + keyName + ".ak)");
166+
}
167+
else
168+
{
169+
using (BinaryReader reader = new(new FileStream(keyName + ".ak", FileMode.Open)))
170+
key = reader.ReadBytes(16);
171+
172+
KeyService.SetArmadilloKey(keyName, key);
173+
}
174+
}
175+
176+
byte[] IV = Convert.FromHexString(Path.GetFileNameWithoutExtension(name));
177+
Array.Copy(IV, 8, IV, 0, 8);
178+
Array.Resize(ref IV, 8);
179+
180+
output = KeyService.SalsaInstance.CreateDecryptor(key, IV, offset).TransformFinalBlock(data[0..], 0, data.Length);
181+
return true;
182+
183+
}
155184
}
156185
}

TACTSharp/Build.cs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
namespace TACTSharp
1+
using System.Text.Json;
2+
using System.Text.Json.Nodes;
3+
4+
namespace TACTSharp
25
{
36
public class BuildInstance
47
{
58
public Config? BuildConfig { get; private set; }
69
public Config? CDNConfig { get; private set; }
10+
public string? ProductConfig { get; private set; }
711

812
public EncodingInstance? Encoding { get; private set; }
913
public RootInstance? Root { get; private set; }
@@ -20,6 +24,8 @@ public BuildInstance()
2024
cdn = new(Settings);
2125
}
2226

27+
public void ResetCDN() => cdn = new(Settings);
28+
2329
public void LoadConfigs(string buildConfig, string cdnConfig)
2430
{
2531
Settings.BuildConfig = buildConfig;
@@ -32,18 +38,58 @@ public void LoadConfigs(string buildConfig, string cdnConfig)
3238
BuildConfig = new Config(cdn, buildConfig, true);
3339
else if (buildConfig.Length == 32 && buildConfig.All(c => "0123456789abcdef".Contains(c)))
3440
BuildConfig = new Config(cdn, buildConfig, false);
41+
else
42+
throw new Exception("Invalid build config, must be a file path or a 32 character hex string");
3543

3644
if (File.Exists(cdnConfig))
3745
CDNConfig = new Config(cdn, cdnConfig, true);
3846
else if (cdnConfig.Length == 32 && cdnConfig.All(c => "0123456789abcdef".Contains(c)))
3947
CDNConfig = new Config(cdn, cdnConfig, false);
48+
else
49+
throw new Exception("Invalid CDN config, must be a file path or a 32 character hex string");
4050

4151
if (BuildConfig == null || CDNConfig == null)
4252
throw new Exception("Failed to load configs");
4353
timer.Stop();
4454
Console.WriteLine("Configs loaded in " + Math.Ceiling(timer.Elapsed.TotalMilliseconds) + "ms");
4555
}
4656

57+
public void LoadConfigs(string buildConfig, string cdnConfig, string productConfig)
58+
{
59+
Settings.ProductConfig = productConfig;
60+
61+
var timer = new System.Diagnostics.Stopwatch();
62+
timer.Start();
63+
64+
ProductConfig = cdn.GetProductConfig(productConfig);
65+
66+
if (ProductConfig == null)
67+
throw new Exception("Failed to load product config");
68+
69+
try
70+
{
71+
var parsedJSON = JsonSerializer.Deserialize<JsonNode>(ProductConfig);
72+
if (parsedJSON == null || parsedJSON["all"] == null || parsedJSON["all"]!["config"] == null)
73+
throw new Exception("Product config has missing keys");
74+
75+
if (parsedJSON["all"]!["config"]!["decryption_key_name"] != null && !string.IsNullOrEmpty(parsedJSON["all"]!["config"]!["decryption_key_name"]!.GetValue<string>()))
76+
{
77+
cdn.ArmadilloKeyName = parsedJSON["all"]!["config"]!["decryption_key_name"]!.GetValue<string>();
78+
Console.WriteLine("Set Armadillo key name to " + cdn.ArmadilloKeyName);
79+
}
80+
}
81+
catch (Exception ex)
82+
{
83+
Console.WriteLine("Failed to parse product config: " + ex.Message);
84+
}
85+
86+
timer.Stop();
87+
88+
Console.WriteLine("Product config loaded in " + Math.Ceiling(timer.Elapsed.TotalMilliseconds) + "ms");
89+
90+
LoadConfigs(buildConfig, cdnConfig);
91+
}
92+
4793
public void Load()
4894
{
4995
if (BuildConfig == null || CDNConfig == null)

TACTSharp/CDN.cs

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public class CDN
1818
// However, if ProductDirectory is accessed before the first CDN is loaded (or if not set through .build.info loading) it'll be null.
1919
public string ProductDirectory = string.Empty;
2020

21+
public string ArmadilloKeyName = string.Empty;
22+
2123
// TODO: Memory mapped cache file access?
2224
public CDN(Settings settings)
2325
{
@@ -213,6 +215,8 @@ public async Task<string> GetPatchServiceFile(string product, string file = "ver
213215

214216
if (!string.IsNullOrEmpty(Settings.CDNDir))
215217
{
218+
// TODO: How do we handle encrypted local CDN copies?
219+
216220
var cdnPath = Path.Combine(Settings.CDNDir, ProductDirectory, type, $"{hash[0]}{hash[1]}", $"{hash[2]}{hash[3]}", hash);
217221
FileLocks.TryAdd(cdnPath, new Lock());
218222

@@ -253,7 +257,25 @@ public async Task<string> GetPatchServiceFile(string product, string file = "ver
253257
{
254258
using (var fileStream = new FileStream(cachePath, FileMode.Create, FileAccess.Write))
255259
{
256-
response.Content.ReadAsStream(token).CopyTo(fileStream);
260+
if (string.IsNullOrEmpty(ArmadilloKeyName))
261+
{
262+
response.Content.ReadAsStream(token).CopyTo(fileStream);
263+
}
264+
else
265+
{
266+
using (var ms = new MemoryStream())
267+
{
268+
response.Content.ReadAsStream(token).CopyTo(ms);
269+
ms.Position = 0;
270+
if (!BLTE.TryDecryptArmadillo(hash, ArmadilloKeyName, ms.ToArray(), out var output))
271+
{
272+
Console.WriteLine("Failed to decrypt file " + hash + " downloaded from " + CDNServers[i]);
273+
File.Delete(cachePath);
274+
continue;
275+
}
276+
fileStream.Write(output);
277+
}
278+
}
257279
}
258280
}
259281
catch (Exception e)
@@ -351,6 +373,8 @@ public unsafe bool TryGetLocalFile(string eKey, out ReadOnlySpan<byte> data)
351373

352374
if (!string.IsNullOrEmpty(Settings.CDNDir))
353375
{
376+
// TODO: How do we handle encrypted local CDN copies?
377+
354378
var cdnPath = Path.Combine(Settings.CDNDir, ProductDirectory, "data", $"{archive[0]}{archive[1]}", $"{archive[2]}{archive[3]}", archive);
355379
FileLocks.TryAdd(cdnPath, new Lock());
356380
if (File.Exists(cdnPath))
@@ -416,7 +440,25 @@ public unsafe bool TryGetLocalFile(string eKey, out ReadOnlySpan<byte> data)
416440
{
417441
using (var fileStream = new FileStream(cachePath, FileMode.Create, FileAccess.Write))
418442
{
419-
response.Content.ReadAsStream(token).CopyTo(fileStream);
443+
if (string.IsNullOrEmpty(ArmadilloKeyName))
444+
{
445+
response.Content.ReadAsStream(token).CopyTo(fileStream);
446+
}
447+
else
448+
{
449+
using (var ms = new MemoryStream())
450+
{
451+
response.Content.ReadAsStream(token).CopyTo(ms);
452+
ms.Position = 0;
453+
if (!BLTE.TryDecryptArmadillo(archive, ArmadilloKeyName, ms.ToArray(), out var output, offset))
454+
{
455+
Console.WriteLine("Failed to decrypt file " + eKey + " from archive " + archive + " downloaded from " + CDNServers[i]);
456+
File.Delete(cachePath);
457+
continue;
458+
}
459+
fileStream.Write(output);
460+
}
461+
}
420462
}
421463
}
422464
catch (Exception ex)
@@ -489,5 +531,60 @@ public unsafe bool TryGetLocalFile(string eKey, out ReadOnlySpan<byte> data)
489531

490532
return cachePath;
491533
}
534+
535+
public string GetProductConfig(string hash, CancellationToken token = new())
536+
{
537+
lock (cdnLock)
538+
{
539+
if (CDNServers.Count == 0)
540+
{
541+
LoadCDNs();
542+
}
543+
}
544+
545+
var cachePath = Path.Combine(Settings.CacheDir, "tpr/configs/data", hash);
546+
FileLocks.TryAdd(cachePath, new Lock());
547+
548+
if (File.Exists(cachePath))
549+
return File.ReadAllText(cachePath);
550+
551+
for (var i = 0; i < CDNServers.Count; i++)
552+
{
553+
var url = $"http://{CDNServers[i]}/tpr/configs/data/{hash[0]}{hash[1]}/{hash[2]}{hash[3]}/{hash}";
554+
555+
Console.WriteLine("Downloading " + url);
556+
557+
var request = new HttpRequestMessage(HttpMethod.Get, url);
558+
559+
var response = Client.Send(request, token);
560+
561+
if (!response.IsSuccessStatusCode)
562+
{
563+
Console.WriteLine("Encountered HTTP " + response.StatusCode + " downloading " + hash + " from " + CDNServers[i]);
564+
continue;
565+
}
566+
567+
lock (FileLocks[cachePath])
568+
{
569+
Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!);
570+
571+
try
572+
{
573+
using (var fileStream = new FileStream(cachePath, FileMode.Create, FileAccess.Write))
574+
response.Content.ReadAsStream(token).CopyTo(fileStream);
575+
}
576+
catch (Exception e)
577+
{
578+
Console.WriteLine("Failed to download file: " + e.Message);
579+
File.Delete(cachePath);
580+
continue;
581+
}
582+
}
583+
584+
return File.ReadAllText(cachePath);
585+
}
586+
587+
throw new FileNotFoundException("Exhausted all CDNs trying to download " + hash);
588+
}
492589
}
493590
}

TACTSharp/Settings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public class Settings
99
public string? BaseDir;
1010
public string? BuildConfig;
1111
public string? CDNConfig;
12+
public string? ProductConfig;
1213
public string CacheDir = "cache";
1314
public string CDNDir = "";
1415
public bool ListfileFallback = true;

TACTSharp/Utils/KeyService.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ static KeyService()
99
}
1010

1111
private static readonly Dictionary<ulong, byte[]> keys = [];
12+
private static readonly Dictionary<string, byte[]> ArmadilloKeys = new();
1213

1314
public static Salsa20 SalsaInstance { get; } = new Salsa20();
1415

16+
// TACT keys
1517
public static bool TryGetKey(ulong keyName, out byte[] key)
1618
{
1719
#pragma warning disable CS8601 // Possible null reference assignment.
@@ -21,12 +23,30 @@ public static bool TryGetKey(ulong keyName, out byte[] key)
2123

2224
public static void SetKey(ulong keyName, byte[] key)
2325
{
24-
if(key == null || key.Length == 0)
26+
if (key == null || key.Length == 0)
2527
throw new ArgumentException("Key cannot be null or empty", nameof(key));
2628

2729
keys[keyName] = key;
2830
}
2931

32+
// Armadillo keys
33+
public static bool TryGetArmadilloKey(string name, out byte[] key)
34+
{
35+
#pragma warning disable CS8601 // Possible null reference assignment.
36+
return ArmadilloKeys.TryGetValue(name, out key);
37+
#pragma warning restore CS8601
38+
}
39+
40+
public static void SetArmadilloKey(string name, byte[] key)
41+
{
42+
if (string.IsNullOrEmpty(name))
43+
throw new ArgumentException("Name cannot be null or empty", nameof(name));
44+
if (key == null || key.Length == 0)
45+
throw new ArgumentException("Key cannot be null or empty", nameof(key));
46+
47+
ArmadilloKeys[name] = key;
48+
}
49+
3050
public static void LoadKeys()
3151
{
3252
if (!File.Exists("WoW.txt")) return;

0 commit comments

Comments
 (0)