runtime: add runtime-key model and delivery strategies
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) <noreply@anthropic.com>
This commit is contained in:
30
src/Metin2Launcher/Runtime/EnvVarKeyDelivery.cs
Normal file
30
src/Metin2Launcher/Runtime/EnvVarKeyDelivery.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Metin2Launcher.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="ProcessStartInfo.Environment"/>, not the launcher's
|
||||
/// global process environment), so other processes on the machine never see them.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
18
src/Metin2Launcher/Runtime/IRuntimeKeyDelivery.cs
Normal file
18
src/Metin2Launcher/Runtime/IRuntimeKeyDelivery.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Metin2Launcher.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Strategy for handing a <see cref="RuntimeKey"/> to the game process. The
|
||||
/// MVP implements a single strategy (<see cref="EnvVarKeyDelivery"/>) — the
|
||||
/// Windows shared-memory strategy is stubbed in <c>SharedMemoryKeyDelivery</c>
|
||||
/// and will be wired once the client-side receiver lands.
|
||||
/// </summary>
|
||||
public interface IRuntimeKeyDelivery
|
||||
{
|
||||
/// <summary>Name of the delivery mechanism, used for logs and tests.</summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>Mutates <paramref name="psi"/> so the spawned process can pick up the key.</summary>
|
||||
void Apply(ProcessStartInfo psi, RuntimeKey key);
|
||||
}
|
||||
76
src/Metin2Launcher/Runtime/RuntimeKey.cs
Normal file
76
src/Metin2Launcher/Runtime/RuntimeKey.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Metin2Launcher.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="IRuntimeKeyDelivery"/>
|
||||
/// 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):
|
||||
/// <code>
|
||||
/// {
|
||||
/// "key_id": "2026.04.14-1",
|
||||
/// "master_key_hex": "<64 hex chars>",
|
||||
/// "sign_pubkey_hex": "<64 hex chars>"
|
||||
/// }
|
||||
/// </code>
|
||||
/// </summary>
|
||||
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,
|
||||
};
|
||||
|
||||
/// <summary>Parses a runtime-key.json blob. Throws on malformed or incomplete data.</summary>
|
||||
public static RuntimeKey Parse(ReadOnlySpan<byte> raw)
|
||||
{
|
||||
var rk = JsonSerializer.Deserialize<RuntimeKey>(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;
|
||||
}
|
||||
}
|
||||
39
src/Metin2Launcher/Runtime/SharedMemoryKeyDelivery.cs
Normal file
39
src/Metin2Launcher/Runtime/SharedMemoryKeyDelivery.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Metin2Launcher.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="Program"/> 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 <c>MemoryMappedFile</c> with a random suffix
|
||||
/// (e.g. <c>m2pack-key-<guid></c>) and a <c>NamedPipeServerStream</c>-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
|
||||
/// <c>OpenExisting</c> 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 <c>ClientAppliedReporter</c>).
|
||||
///
|
||||
/// Until then, calling <see cref="Apply"/> 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.
|
||||
/// </summary>
|
||||
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.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user