From dcc2b0fc422d88e00dfa6f387ba6beee4b7a3768 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 14 Apr 2026 21:05:37 +0200 Subject: [PATCH] runtime: add runtime-key model and delivery strategies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit adds RuntimeKey DTO parsed from runtime-key.json, an IRuntimeKeyDelivery strategy interface, and the env-var delivery used by the m2pack MVP (M2PACK_MASTER_KEY_HEX / M2PACK_SIGN_PUBKEY_HEX / M2PACK_KEY_ID). SharedMemoryKeyDelivery is a documented stub — will be wired once the client-side receiver lands. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Runtime/EnvVarKeyDelivery.cs | 30 ++++++++ .../Runtime/IRuntimeKeyDelivery.cs | 18 +++++ src/Metin2Launcher/Runtime/RuntimeKey.cs | 76 +++++++++++++++++++ .../Runtime/SharedMemoryKeyDelivery.cs | 39 ++++++++++ 4 files changed, 163 insertions(+) create mode 100644 src/Metin2Launcher/Runtime/EnvVarKeyDelivery.cs create mode 100644 src/Metin2Launcher/Runtime/IRuntimeKeyDelivery.cs create mode 100644 src/Metin2Launcher/Runtime/RuntimeKey.cs create mode 100644 src/Metin2Launcher/Runtime/SharedMemoryKeyDelivery.cs diff --git a/src/Metin2Launcher/Runtime/EnvVarKeyDelivery.cs b/src/Metin2Launcher/Runtime/EnvVarKeyDelivery.cs new file mode 100644 index 0000000..4034de7 --- /dev/null +++ b/src/Metin2Launcher/Runtime/EnvVarKeyDelivery.cs @@ -0,0 +1,30 @@ +using System.Diagnostics; + +namespace Metin2Launcher.Runtime; + +/// +/// Delivers the runtime key to the game process via environment variables. +/// This is the MVP mechanism — every platform supports it, no native interop, +/// no timing windows. The variables are scoped to the child's environment only +/// (we mutate , not the launcher's +/// global process environment), so other processes on the machine never see them. +/// +public sealed class EnvVarKeyDelivery : IRuntimeKeyDelivery +{ + public const string MasterKeyVar = "M2PACK_MASTER_KEY_HEX"; + public const string SignPubkeyVar = "M2PACK_SIGN_PUBKEY_HEX"; + public const string KeyIdVar = "M2PACK_KEY_ID"; + + public string Name => "env-var"; + + public void Apply(ProcessStartInfo psi, RuntimeKey key) + { + if (psi is null) throw new ArgumentNullException(nameof(psi)); + if (key is null) throw new ArgumentNullException(nameof(key)); + + // Never set these on the launcher process itself — scope them to the child only. + psi.Environment[MasterKeyVar] = key.MasterKeyHex; + psi.Environment[SignPubkeyVar] = key.SignPubkeyHex; + psi.Environment[KeyIdVar] = key.KeyId; + } +} diff --git a/src/Metin2Launcher/Runtime/IRuntimeKeyDelivery.cs b/src/Metin2Launcher/Runtime/IRuntimeKeyDelivery.cs new file mode 100644 index 0000000..bac6681 --- /dev/null +++ b/src/Metin2Launcher/Runtime/IRuntimeKeyDelivery.cs @@ -0,0 +1,18 @@ +using System.Diagnostics; + +namespace Metin2Launcher.Runtime; + +/// +/// Strategy for handing a to the game process. The +/// MVP implements a single strategy () — the +/// Windows shared-memory strategy is stubbed in SharedMemoryKeyDelivery +/// and will be wired once the client-side receiver lands. +/// +public interface IRuntimeKeyDelivery +{ + /// Name of the delivery mechanism, used for logs and tests. + string Name { get; } + + /// Mutates so the spawned process can pick up the key. + void Apply(ProcessStartInfo psi, RuntimeKey key); +} diff --git a/src/Metin2Launcher/Runtime/RuntimeKey.cs b/src/Metin2Launcher/Runtime/RuntimeKey.cs new file mode 100644 index 0000000..25a214d --- /dev/null +++ b/src/Metin2Launcher/Runtime/RuntimeKey.cs @@ -0,0 +1,76 @@ +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; + } +} diff --git a/src/Metin2Launcher/Runtime/SharedMemoryKeyDelivery.cs b/src/Metin2Launcher/Runtime/SharedMemoryKeyDelivery.cs new file mode 100644 index 0000000..d4726dd --- /dev/null +++ b/src/Metin2Launcher/Runtime/SharedMemoryKeyDelivery.cs @@ -0,0 +1,39 @@ +using System.Diagnostics; + +namespace Metin2Launcher.Runtime; + +/// +/// STUB. Windows shared-memory delivery for the runtime key is not implemented +/// yet — the receiver on the client side has not landed. This type exists so +/// the factory wiring in and the docs have a concrete +/// name to reference, and so a future change can fill in the body without +/// churning callers. +/// +/// Planned implementation: +/// 1. Create a named, write-only MemoryMappedFile with a random suffix +/// (e.g. m2pack-key-<guid>) and a NamedPipeServerStream-style +/// ACL that only grants access to the child PID once it has been spawned. +/// 2. Write a short binary header (magic, key_id length, key_id, master_key, +/// sign_pubkey) into the mapping. +/// 3. Set an env var on the child with the mapping name so the receiver can +/// OpenExisting and read the key before zeroing the source. +/// 4. Zero and close the mapping from the launcher side once the child +/// signals it has consumed the key (via ClientAppliedReporter). +/// +/// Until then, calling throws — the factory must never +/// select this delivery on a non-Windows host, and on Windows the code path +/// is guarded behind a feature flag that is off by default. +/// +public sealed class SharedMemoryKeyDelivery : IRuntimeKeyDelivery +{ + public string Name => "shared-memory"; + + public void Apply(ProcessStartInfo psi, RuntimeKey key) + { + // TODO(m2pack): implement Windows shared-memory delivery once the + // client-side receiver is in place. Tracked in docs/m2pack-integration.md. + throw new NotSupportedException( + "shared-memory runtime key delivery is not implemented yet; " + + "use EnvVarKeyDelivery for the MVP."); + } +}