using System.Text.Json; using System.Text.Json.Serialization; namespace Metin2Launcher.Runtime; /// /// In-memory representation of a runtime key bundle delivered alongside an /// m2pack release. The launcher never decrypts or opens .m2p archives itself; /// it merely parses the key bundle produced by the release tool and passes it /// through to the game executable via one of the /// mechanisms. /// /// File layout (runtime-key.json, placed next to the manifest by the release /// tool, never signed independently — its integrity comes from the signed /// manifest that references it): /// /// { /// "key_id": "2026.04.14-1", /// "master_key_hex": "<64 hex chars>", /// "sign_pubkey_hex": "<64 hex chars>" /// } /// /// public sealed class RuntimeKey { [JsonPropertyName("key_id")] public string KeyId { get; set; } = ""; [JsonPropertyName("master_key_hex")] public string MasterKeyHex { get; set; } = ""; [JsonPropertyName("sign_pubkey_hex")] public string SignPubkeyHex { get; set; } = ""; private static readonly JsonSerializerOptions _opts = new() { PropertyNameCaseInsensitive = false, }; /// Parses a runtime-key.json blob. Throws on malformed or incomplete data. public static RuntimeKey Parse(ReadOnlySpan raw) { var rk = JsonSerializer.Deserialize(raw, _opts) ?? throw new InvalidDataException("runtime-key deserialized to null"); Validate(rk); return rk; } public static RuntimeKey Load(string path) { var bytes = File.ReadAllBytes(path); return Parse(bytes); } private static void Validate(RuntimeKey rk) { if (string.IsNullOrWhiteSpace(rk.KeyId)) throw new InvalidDataException("runtime-key missing 'key_id'"); if (!IsHex(rk.MasterKeyHex, 64)) throw new InvalidDataException("runtime-key 'master_key_hex' must be 64 hex chars"); if (!IsHex(rk.SignPubkeyHex, 64)) throw new InvalidDataException("runtime-key 'sign_pubkey_hex' must be 64 hex chars"); } private static bool IsHex(string s, int expectedLen) { if (s is null || s.Length != expectedLen) return false; for (int i = 0; i < s.Length; i++) { char c = s[i]; bool ok = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); if (!ok) return false; } return true; } }