mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 07:29:00 -07:00
188 lines
7.0 KiB
C#
188 lines
7.0 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.Buffers.Binary;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
|
|
|
|
public static class ImageSignatureRequest
|
|
{
|
|
|
|
|
|
|
|
const int HeaderScanLimit = 0x100;
|
|
|
|
const int MinimumRecords = 32;
|
|
|
|
|
|
|
|
|
|
const int PrefixProbe = 8;
|
|
|
|
const int PackingProbe = 1024;
|
|
|
|
const int MaxLiteralLength = 1 << 20;
|
|
|
|
static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(false, true);
|
|
|
|
|
|
public static string? DetectKeyId(byte[]? metadata)
|
|
{
|
|
if (metadata == null || metadata.Length < 24 ||
|
|
BinaryPrimitives.ReadUInt32LittleEndian(metadata) != 0xFAB11BAF ||
|
|
BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(4)) != 27)
|
|
return null;
|
|
int table = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(8));
|
|
int size = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(12));
|
|
int data = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(16));
|
|
int dataSize = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(20));
|
|
if (table < 24 || size < 0 || size % 8 != 0 || (long)table + size > metadata.Length ||
|
|
data < 24 || dataSize < 0 || (long)data + dataSize > metadata.Length)
|
|
return null;
|
|
|
|
return ScanKeyTails(metadata, table, size / 8, data, dataSize);
|
|
}
|
|
|
|
|
|
|
|
public static string? DetectKeyIdFromGameMetadata(byte[]? metadata)
|
|
{
|
|
if (metadata == null || metadata.Length < 24) return null;
|
|
if (BinaryPrimitives.ReadUInt32LittleEndian(metadata) == 0xFAB11BAF &&
|
|
BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(4)) == 27)
|
|
return DetectKeyId(metadata);
|
|
return DetectKeyIdByHeaderScan(metadata);
|
|
}
|
|
|
|
|
|
|
|
|
|
static string? ScanKeyTails(byte[] metadata, int tableOffset, int recordCount, int dataOffset, int dataSize)
|
|
{
|
|
string? key = null;
|
|
for (int record = 0; record < recordCount; record++)
|
|
{
|
|
var (length, index) = Record(metadata, tableOffset, record);
|
|
if (length < 0 || index < 0 || (long)index + length > dataSize) return null;
|
|
if (length != 8 && length != 18) continue;
|
|
var literal = Encoding.UTF8.GetString(metadata, dataOffset + index, length);
|
|
if (literal != ".rec.net" && literal != ".radicade.internal") continue;
|
|
var candidate = "KEY:RSA:p1" + literal;
|
|
if (key != null && key != candidate) return null;
|
|
key = candidate;
|
|
}
|
|
return key;
|
|
}
|
|
|
|
|
|
|
|
|
|
static (int Length, int Index) Record(byte[] metadata, int tableOffset, int record)
|
|
{
|
|
int offset = tableOffset + 8 * record;
|
|
return (BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(offset)),
|
|
BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(offset + 4)));
|
|
}
|
|
|
|
public static string? DetectKeyIdByHeaderScan(byte[]? metadata)
|
|
{
|
|
if (metadata == null || metadata.Length < HeaderScanLimit + 8) return null;
|
|
|
|
var fields = new List<int>();
|
|
for (int offset = 0; offset + 4 <= HeaderScanLimit; offset += 4)
|
|
{
|
|
int value = BinaryPrimitives.ReadInt32LittleEndian(metadata.AsSpan(offset));
|
|
if (value > 0 && value <= metadata.Length && !fields.Contains(value)) fields.Add(value);
|
|
}
|
|
|
|
var tables = new List<(int Offset, int Count)>();
|
|
foreach (var tableOffset in fields)
|
|
foreach (var tableSize in fields)
|
|
{
|
|
if (tableSize % 8 != 0 || tableSize / 8 < MinimumRecords) continue;
|
|
if (tableOffset < HeaderScanLimit || (long)tableOffset + tableSize > metadata.Length) continue;
|
|
tables.Add((tableOffset, tableSize / 8));
|
|
}
|
|
|
|
var slices = new List<(int Offset, int Size)>();
|
|
foreach (var dataOffset in fields)
|
|
foreach (var dataSize in fields)
|
|
{
|
|
if (dataSize <= 0 || dataOffset < HeaderScanLimit) continue;
|
|
if ((long)dataOffset + dataSize > metadata.Length) continue;
|
|
slices.Add((dataOffset, dataSize));
|
|
}
|
|
|
|
|
|
|
|
tables.Sort((a, b) => b.Count.CompareTo(a.Count));
|
|
|
|
foreach (var (tableOffset, count) in tables)
|
|
foreach (var (dataOffset, dataSize) in slices)
|
|
{
|
|
if (!LooksLikeLiteralTable(metadata, tableOffset, count, dataOffset, dataSize, PrefixProbe)) continue;
|
|
if (!SampleDecodesAsText(metadata, tableOffset, count, dataOffset, dataSize)) continue;
|
|
if (!LooksLikeLiteralTable(metadata, tableOffset, count, dataOffset, dataSize, PackingProbe)) continue;
|
|
var key = ScanKeyTails(metadata, tableOffset, count, dataOffset, dataSize);
|
|
if (key != null) return key;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool LooksLikeLiteralTable(byte[] metadata, int tableOffset, int count, int dataOffset, int dataSize, int probe)
|
|
{
|
|
int examined = Math.Min(count, probe);
|
|
int contiguous = 0;
|
|
long running = 0;
|
|
for (int record = 0; record < examined; record++)
|
|
{
|
|
var (length, index) = Record(metadata, tableOffset, record);
|
|
if (length < 0 || length > MaxLiteralLength || index < 0 || (long)index + length > dataSize) return false;
|
|
if (index == running) contiguous++;
|
|
running = index + length;
|
|
}
|
|
return contiguous >= examined - Math.Max(1, examined / 100);
|
|
}
|
|
|
|
static bool SampleDecodesAsText(byte[] metadata, int tableOffset, int count, int dataOffset, int dataSize)
|
|
{
|
|
int step = Math.Max(1, count / 48);
|
|
for (int record = 0; record < count; record += step)
|
|
{
|
|
var (length, index) = Record(metadata, tableOffset, record);
|
|
if (length < 0 || index < 0 || (long)index + length > dataSize) return false;
|
|
if (length == 0) continue;
|
|
try
|
|
{
|
|
StrictUtf8.GetCharCount(metadata, dataOffset + index, length);
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public static string AddKeyId(string url, string serverUrl, string? keyId)
|
|
{
|
|
if (string.IsNullOrEmpty(keyId) ||
|
|
!Uri.TryCreate(url, UriKind.Absolute, out var request) ||
|
|
!Uri.TryCreate(serverUrl, UriKind.Absolute, out var server) ||
|
|
(server.Scheme != "http" && server.Scheme != "https") ||
|
|
request.Scheme != server.Scheme || request.Authority != server.Authority ||
|
|
!request.AbsolutePath.StartsWith(server.AbsolutePath.TrimEnd('/') + "/images/", StringComparison.OrdinalIgnoreCase))
|
|
return url;
|
|
|
|
|
|
var parameters = request.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries)
|
|
.Where(p => !Uri.UnescapeDataString(p.Split('=')[0]).Equals("rcsig", StringComparison.OrdinalIgnoreCase))
|
|
.Append("rcsig=" + Uri.EscapeDataString(keyId));
|
|
return request.GetLeftPart(UriPartial.Path) + "?" + string.Join("&", parameters) + request.Fragment;
|
|
}
|
|
}
|