#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(); 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; } }