The canonical launcher runtime-key.json shape is
{ key_id: string, master_key_hex, sign_pubkey_hex }
but `m2pack export-runtime-key --format json` emits
{ version, mapping_name, key_id: int, master_key_hex, sign_public_key_hex }
because its JSON is really a dump of the Windows shared-memory struct.
Parsing the CLI output with the old strict deserializer throws
JsonException on key_id (int != string) and silently drops the public
key field (name mismatch), after which Validate() rejects the key as
not 64 hex chars and the m2pack release fails to boot with
"runtime master key with key_id=1 required for 'pack/root.m2p'".
Hit this tonight during the 2026.04.15-m2pack-v2 release and worked
around it by hand-writing runtime-key.json. Fix: parse into a
JsonElement and extract fields tolerantly — key_id accepts either a
JSON string or a JSON number (stringified), and the pubkey field is
looked up under both "sign_pubkey_hex" and "sign_public_key_hex".
Added a test covering the m2pack CLI shape end to end. Also kept the
malformed-input path on JsonSerializer.Deserialize so it still throws
JsonException (JsonDocument.Parse throws its internal subtype which
breaks Assert.Throws<JsonException>).
Tracked separately as metin-server/m2pack-secure#3 — the m2pack side
should also align its JSON to the canonical shape; this commit is the
client-side belt to the server-side suspenders.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
107 lines
3.2 KiB
C#
107 lines
3.2 KiB
C#
using System.Text;
|
|
using Metin2Launcher.Runtime;
|
|
using Xunit;
|
|
|
|
namespace Metin2Launcher.Tests;
|
|
|
|
public class RuntimeKeyTests
|
|
{
|
|
private const string ValidHex64 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
|
|
|
private static string SampleJson(
|
|
string keyId = "2026.04.14-1",
|
|
string master = ValidHex64,
|
|
string pub = ValidHex64)
|
|
=> $$"""
|
|
{
|
|
"key_id": "{{keyId}}",
|
|
"master_key_hex": "{{master}}",
|
|
"sign_pubkey_hex": "{{pub}}"
|
|
}
|
|
""";
|
|
|
|
[Fact]
|
|
public void Parse_happy_path()
|
|
{
|
|
var rk = RuntimeKey.Parse(Encoding.UTF8.GetBytes(SampleJson()));
|
|
Assert.Equal("2026.04.14-1", rk.KeyId);
|
|
Assert.Equal(ValidHex64, rk.MasterKeyHex);
|
|
Assert.Equal(ValidHex64, rk.SignPubkeyHex);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_rejects_missing_key_id()
|
|
{
|
|
var json = SampleJson(keyId: "");
|
|
Assert.Throws<InvalidDataException>(() => RuntimeKey.Parse(Encoding.UTF8.GetBytes(json)));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("")]
|
|
[InlineData("tooshort")]
|
|
[InlineData("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz")] // right len, wrong chars
|
|
public void Parse_rejects_bad_master_key(string master)
|
|
{
|
|
var json = SampleJson(master: master);
|
|
Assert.Throws<InvalidDataException>(() => RuntimeKey.Parse(Encoding.UTF8.GetBytes(json)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_rejects_bad_pubkey()
|
|
{
|
|
var json = SampleJson(pub: new string('g', 64));
|
|
Assert.Throws<InvalidDataException>(() => RuntimeKey.Parse(Encoding.UTF8.GetBytes(json)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_accepts_uppercase_hex()
|
|
{
|
|
var upper = ValidHex64.ToUpperInvariant();
|
|
var rk = RuntimeKey.Parse(Encoding.UTF8.GetBytes(SampleJson(master: upper, pub: upper)));
|
|
Assert.Equal(upper, rk.MasterKeyHex);
|
|
}
|
|
|
|
[Fact]
|
|
public void Load_reads_from_disk()
|
|
{
|
|
var tmp = Path.Combine(Path.GetTempPath(), "runtime-key-test-" + Guid.NewGuid() + ".json");
|
|
File.WriteAllText(tmp, SampleJson());
|
|
try
|
|
{
|
|
var rk = RuntimeKey.Load(tmp);
|
|
Assert.Equal("2026.04.14-1", rk.KeyId);
|
|
}
|
|
finally { File.Delete(tmp); }
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_rejects_malformed_json()
|
|
{
|
|
Assert.Throws<System.Text.Json.JsonException>(() =>
|
|
RuntimeKey.Parse(Encoding.UTF8.GetBytes("not json")));
|
|
}
|
|
|
|
// m2pack export-runtime-key emits key_id as an integer and the public
|
|
// key field as `sign_public_key_hex`, plus extra version/mapping_name
|
|
// fields that are Windows-shared-memory bookkeeping. RuntimeKey must
|
|
// tolerate that shape so release pipelines can pipe the CLI output
|
|
// directly. See metin-server/m2pack-secure#3.
|
|
[Fact]
|
|
public void Parse_accepts_m2pack_cli_shape()
|
|
{
|
|
var json = $$"""
|
|
{
|
|
"version": 1,
|
|
"mapping_name": "Local\\M2PackSharedKeys",
|
|
"key_id": 1,
|
|
"master_key_hex": "{{ValidHex64}}",
|
|
"sign_public_key_hex": "{{ValidHex64}}"
|
|
}
|
|
""";
|
|
var rk = RuntimeKey.Parse(Encoding.UTF8.GetBytes(json));
|
|
Assert.Equal("1", rk.KeyId);
|
|
Assert.Equal(ValidHex64, rk.MasterKeyHex);
|
|
Assert.Equal(ValidHex64, rk.SignPubkeyHex);
|
|
}
|
|
}
|