Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of RunicSentinel v1.2.0
RunicSentinel.dll
Decompiled 32 minutes ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using RunicSafety.Api; using RunicSentinel.Api; using RunicSentinel.Contracts; using RunicSentinel.Core; using RunicSentinel.Runtime; using Steamworks; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("Runic Sentinel")] [assembly: AssemblyDescription("Pinned-RSA policy verification, direct optional/required admission claims, and bounded transport-identity evidence")] [assembly: AssemblyCompany("Chazman")] [assembly: AssemblyProduct("Runic Sentinel")] [assembly: AssemblyFileVersion("1.2.0.0")] [assembly: AssemblyInformationalVersion("1.2.0")] [assembly: InternalsVisibleTo("RunicSentinel.Tests")] [assembly: InternalsVisibleTo("RunicSentinel.Forge")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyVersion("1.2.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace RunicSentinel { internal static class SentinelConfig { internal static ConfigEntry<bool> Enabled; internal static ConfigEntry<string> PolicyFile; internal static ConfigEntry<string> SignatureFile; internal static ConfigEntry<string> PublicKeyFile; internal static ConfigEntry<string> TrustedPublicKeySha256; internal static ConfigEntry<string> RemoteAdmissionPolicy; internal static ConfigEntry<int> IntegrityCheckSeconds; internal static ConfigEntry<bool> BackupBeforeTransitions; internal static ConfigEntry<int> VeryHighDisconnectCount; internal static ConfigEntry<int> HighDisconnectCount; internal static ConfigEntry<int> EnforcementWindowSeconds; internal static ConfigEntry<KeyboardShortcut> AdminPanelKey; internal static SentinelRemoteAdmissionMode RemoteAdmissionMode { get { if (!string.Equals(RemoteAdmissionPolicy?.Value?.Trim(), "Disabled", StringComparison.OrdinalIgnoreCase)) { if (!string.Equals(RemoteAdmissionPolicy?.Value?.Trim(), "Required", StringComparison.OrdinalIgnoreCase)) { return SentinelRemoteAdmissionMode.Optional; } return SentinelRemoteAdmissionMode.Required; } return SentinelRemoteAdmissionMode.Disabled; } } internal static event Action Changed; internal static void Bind(ConfigFile config) { //IL_010b: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Expected O, but got Unknown //IL_0138: Unknown result type (might be due to invalid IL or missing references) //IL_0142: Expected O, but got Unknown //IL_016a: Unknown result type (might be due to invalid IL or missing references) //IL_0174: Expected O, but got Unknown //IL_018e: Unknown result type (might be due to invalid IL or missing references) Enabled = config.Bind<bool>("General", "Enabled", true, "Enable bounded Sentinel local-snapshot, RSA policy, and evidence services. Sampled at startup; false registers nothing and starts no worker."); PolicyFile = config.Bind<string>("Policy", "ManifestFile", "RunicSentinel.policy", "Canonical RUNIC-SENTINEL/3 policy path, relative to BepInEx/config unless absolute."); SignatureFile = config.Bind<string>("Policy", "SignatureFile", "RunicSentinel.policy.sig", "Canonical Base64 detached RSA-3072/SHA-256 PKCS#1 v1.5 signature path."); PublicKeyFile = config.Bind<string>("Policy", "PublicKeyFile", "RunicSentinel.policy.pub", "Canonical RUNIC-RSA-PUBLIC/1 verification public key path. The optional F3 workflow keeps its private key in a separate server-only directory."); TrustedPublicKeySha256 = config.Bind<string>("Policy", "TrustedPublicKeySha256", string.Empty, "Required lowercase SHA-256 of the exact canonical public-key file. Empty or mismatched pins keep Sentinel monitor-only."); RemoteAdmissionPolicy = config.Bind<string>("Remote Admission", "Policy", "Optional", "Sampled at startup. Required denies missing, stale, malformed, or signed-policy-mismatched self-reported claims by disconnecting the exact authenticated peer. Optional records bounded evidence without disconnecting. Disabled does not register Sentinel handshake claims or evaluators."); IntegrityCheckSeconds = config.Bind<int>("Runtime Integrity", "CheckIntervalSeconds", 15, "Metadata-check loaded plugin DLLs and active signed-passport files at this interval. A detected runtime change denies new strict admissions until restart. Range 5-300 seconds."); BackupBeforeTransitions = config.Bind<bool>("Transition Safety", "BackupWorldBeforeProfileChange", true, "Before a server loads an existing world with a different signed policy or plugin snapshot, require a verified Runic Safety backup of the world database and metadata."); VeryHighDisconnectCount = config.Bind<int>("Automatic Enforcement", "VeryHighFindingsBeforeDisconnect", 2, new ConfigDescription("Disconnect after this many very-high-confidence violations in the enforcement window.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 10), Array.Empty<object>())); HighDisconnectCount = config.Bind<int>("Automatic Enforcement", "HighFindingsBeforeDisconnect", 3, new ConfigDescription("Disconnect after this many high-confidence violations in the enforcement window.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 20), Array.Empty<object>())); EnforcementWindowSeconds = config.Bind<int>("Automatic Enforcement", "FindingWindowSeconds", 60, new ConfigDescription("Rolling violation window used by graduated automatic enforcement.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(10, 600), Array.Empty<object>())); AdminPanelKey = config.Bind<KeyboardShortcut>("Administrator Panel", "OpenPanel", new KeyboardShortcut((KeyCode)284, Array.Empty<KeyCode>()), "Open the server-authorized Runic Sentinel administrator panel. Non-administrators are denied by the server."); PolicyFile.SettingChanged += Notify; SignatureFile.SettingChanged += Notify; PublicKeyFile.SettingChanged += Notify; TrustedPublicKeySha256.SettingChanged += Notify; AdminPanelKey.SettingChanged += Notify; } private static void Notify(object sender, EventArgs args) { SentinelConfig.Changed?.Invoke(); } } [BepInPlugin("chazman.RunicSentinel", "Runic Sentinel", "1.2.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { public const string Guid = "chazman.RunicSentinel"; public const string Name = "Runic Sentinel"; public const string Version = "1.2.0"; public const string ModuleId = "runic.sentinel"; private SentinelRuntime _runtime; private SentinelEnforcementRuntime _enforcement; private SentinelOperatorCommands _operatorCommands; private SentinelManagedPolicyService _managedPolicy; private SentinelAdminControl _adminControl; private SentinelAdminPanel _adminPanel; private SentinelFlightRecorder _flightRecorder; private Harmony _harmony; private int _refreshRequested; private void Awake() { //IL_013f: Unknown result type (might be due to invalid IL or missing references) //IL_0149: Expected O, but got Unknown SentinelConfig.Bind(((BaseUnityPlugin)this).Config); ConfigEntry<bool> enabled = SentinelConfig.Enabled; if (enabled != null && !enabled.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Sentinel is disabled; no worker or network handlers were created."); return; } try { _runtime = new SentinelRuntime(); _runtime.Start(Paths.ConfigPath); _flightRecorder = new SentinelFlightRecorder(_runtime.Evidence, ((BaseUnityPlugin)this).Logger, Paths.ConfigPath); _runtime.AttachNetwork(SentinelConfig.RemoteAdmissionMode); _enforcement = new SentinelEnforcementRuntime(_runtime); SentinelIntegrationApi.Attach(_enforcement); SentinelTransitionBackup.Attach(_runtime, ((BaseUnityPlugin)this).Logger, Paths.ConfigPath); _managedPolicy = new SentinelManagedPolicyService(_runtime, ((BaseUnityPlugin)this).Logger, Paths.ConfigPath, (string reason) => SentinelTransitionBackup.CreateVerifiedBackupNow(reason)); _operatorCommands = new SentinelOperatorCommands(_runtime, ((BaseUnityPlugin)this).Logger, Paths.ConfigPath, _managedPolicy); _adminControl = new SentinelAdminControl(_runtime, _managedPolicy, _operatorCommands); _adminPanel = new SentinelAdminPanel(_adminControl); _harmony = new Harmony("chazman.RunicSentinel"); _harmony.PatchAll(typeof(Plugin).Assembly); SentinelConfig.Changed += Refresh; ((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Sentinel v1.2.0 initialized as a standalone plugin. Signed passports enforce exact plugin profiles, administrators, and banned accounts; F3 administration is authenticated by the current Valheim transport peer. Client file claims remain self-reported compatibility evidence."); } catch (Exception ex) { Shutdown(); ((BaseUnityPlugin)this).Logger.LogError((object)("Runic Sentinel failed closed: " + ex)); } } private void Refresh() { Interlocked.Exchange(ref _refreshRequested, 1); } private void Update() { try { _runtime?.TickNetwork(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Sentinel network request stopped: " + ex.Message)); } try { _runtime?.TickIntegrity(); } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Sentinel runtime-integrity check failed closed: " + ex2.Message)); } try { _adminControl?.Tick(); } catch (Exception ex3) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Sentinel admin transport stopped safely: " + ex3.Message)); } try { _adminPanel?.Tick(); } catch (Exception ex4) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Sentinel administrator panel stopped safely: " + ex4.Message)); } if (Interlocked.Exchange(ref _refreshRequested, 0) == 0 || _runtime == null) { return; } try { _runtime.Start(Paths.ConfigPath); } catch (Exception ex5) { ((BaseUnityPlugin)this).Logger.LogError((object)("Sentinel policy refresh failed closed: " + ex5.Message)); } } private void OnGUI() { try { _adminPanel?.Draw(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Sentinel administrator panel draw failed safely: " + ex.Message)); } } private void OnDestroy() { Shutdown(); } private void Shutdown() { SentinelConfig.Changed -= Refresh; Interlocked.Exchange(ref _refreshRequested, 0); try { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } catch { } _harmony = null; try { _adminPanel?.Dispose(); } catch { } _adminPanel = null; try { _adminControl?.Dispose(); } catch { } _adminControl = null; SentinelTransitionBackup.Detach(); try { _operatorCommands?.Dispose(); } catch { } _operatorCommands = null; _managedPolicy = null; try { _flightRecorder?.Dispose(); } catch { } _flightRecorder = null; SentinelIntegrationApi.Detach(_enforcement); try { _enforcement?.Dispose(); } catch { } _enforcement = null; try { _runtime?.Dispose(); } catch { } _runtime = null; } } } namespace RunicSentinel.Runtime { internal sealed class SentinelAdminControl : IDisposable { private sealed class Pending { internal string Id; internal string Action; internal long Expires; internal Action<bool, SentinelAdminDocument, string> Status; internal Action<bool, string> Result; } private sealed class CachedResponse { internal byte[] RequestDigest; internal bool Accepted; internal string Reason; internal byte[] Payload; internal long Expires; } private const string RequestRpc = "runic.sentinel.admin.request.v1"; private const string ResponseRpc = "runic.sentinel.admin.response.v1"; private const int WireSchema = 1; private const int TerminalMarker = 1369914905; private const int MaximumEnvelopeBytes = 184320; private const int MaximumPending = 16; private const int MaximumReplayEntries = 256; private static readonly long RequestLifetimeTicks = TimeSpan.FromSeconds(30.0).Ticks; private static readonly long ReplayLifetimeTicks = TimeSpan.FromMinutes(1.0).Ticks; private readonly SentinelRuntime _runtime; private readonly SentinelManagedPolicyService _managed; private readonly SentinelOperatorCommands _commands; private readonly Dictionary<string, Pending> _pending = new Dictionary<string, Pending>(StringComparer.Ordinal); private readonly Dictionary<string, CachedResponse> _cache = new Dictionary<string, CachedResponse>(StringComparer.Ordinal); private readonly Queue<string> _cacheOrder = new Queue<string>(); private ZRoutedRpc _registeredRpc; private bool _disposed; internal SentinelAdminControl(SentinelRuntime runtime, SentinelManagedPolicyService managed, SentinelOperatorCommands commands) { _runtime = runtime ?? throw new ArgumentNullException("runtime"); _managed = managed ?? throw new ArgumentNullException("managed"); _commands = commands ?? throw new ArgumentNullException("commands"); } internal void Tick() { if (_disposed) { return; } ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null && instance != _registeredRpc) { instance.Register<ZPackage>("runic.sentinel.admin.request.v1", (Action<long, ZPackage>)ReceiveRequest); instance.Register<ZPackage>("runic.sentinel.admin.response.v1", (Action<long, ZPackage>)ReceiveResponse); _registeredRpc = instance; _pending.Clear(); } long ticks = DateTime.UtcNow.Ticks; foreach (string item in new List<string>(_pending.Keys)) { if (_pending.TryGetValue(item, out var value) && value.Expires <= ticks) { _pending.Remove(item); Fail(value, "The server did not answer the administrator request in time."); } } ExpireCache(ticks); } internal void RequestStatus(Action<bool, SentinelAdminDocument, string> callback) { if (TryExecuteLocal("status", Array.Empty<byte>(), out var accepted, out var response, out var reason)) { callback?.Invoke(accepted, DecodeDocument(response), reason); return; } Submit("status", Array.Empty<byte>(), new Pending { Status = callback }); } internal void Apply(SentinelAdminDocument document, Action<bool, string> callback) { byte[] payload; try { payload = SentinelAdminProtocol.Encode(document); } catch (Exception ex) { callback?.Invoke(arg1: false, ex.Message); return; } if (TryExecuteLocal("apply", payload, out var accepted, out var response, out var reason)) { callback?.Invoke(accepted, accepted ? SentinelAdminProtocol.DecodeMessage(response) : reason); return; } Submit("apply", payload, new Pending { Result = callback }); } internal void RunTool(string tool, Action<bool, string> callback) { byte[] payload; try { payload = SentinelAdminProtocol.EncodeTool(tool); } catch (Exception ex) { callback?.Invoke(arg1: false, ex.Message); return; } if (TryExecuteLocal("tool", payload, out var accepted, out var response, out var reason)) { callback?.Invoke(accepted, accepted ? SentinelAdminProtocol.DecodeMessage(response) : reason); return; } Submit("tool", payload, new Pending { Result = callback }); } private bool TryExecuteLocal(string action, byte[] payload, out bool accepted, out byte[] response, out string reason) { accepted = false; response = Array.Empty<byte>(); reason = string.Empty; ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return false; } if (!SentinelTransportIdentity.TryResolveLocal(out var authority, out var subject)) { reason = "The host backend identity is unavailable."; return true; } Execute(authority, subject, action, payload, out accepted, out response, out reason); return true; } private void Submit(string action, byte[] payload, Pending pending) { if (_disposed || pending == null) { Fail(pending, "Administrator control is unavailable."); return; } ZNet instance = ZNet.instance; ZRoutedRpc registeredRpc = _registeredRpc; ZNetPeer val = ((instance != null) ? instance.GetServerPeer() : null); if ((Object)(object)instance == (Object)null || instance.IsServer() || registeredRpc == null || val == null || !val.IsReady() || _pending.Count >= 16) { Fail(pending, "The authoritative server administrator channel is unavailable."); return; } string text = (pending.Id = Guid.NewGuid().ToString("N")); pending.Action = action; pending.Expires = DateTime.UtcNow.Ticks + RequestLifetimeTicks; _pending.Add(text, pending); ZPackage val2 = WriteRequest(text, action, payload, DateTimeOffset.UtcNow.ToUnixTimeSeconds()); if (val2.Size() > 184320) { _pending.Remove(text); Fail(pending, "Administrator request exceeded its bounded size."); } else { registeredRpc.InvokeRoutedRPC(val.m_uid, "runic.sentinel.admin.request.v1", new object[1] { val2 }); } } private void ReceiveRequest(long sender, ZPackage package) { ZNet instance = ZNet.instance; if (_disposed || (Object)(object)instance == (Object)null || !instance.IsServer() || ZRoutedRpc.instance == null) { return; } ZNetPeer peer = instance.GetPeer(sender); if (peer == null || peer.m_uid != sender || !peer.IsReady()) { return; } if (!TryReadRequest(package, out var id, out var action, out var payload, out var issued)) { SendResponse(sender, string.Empty, accepted: false, "Malformed administrator request.", Array.Empty<byte>()); return; } long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (issued < num - 30 || issued > num + 30) { SendResponse(sender, id, accepted: false, "Administrator request expired.", Array.Empty<byte>()); return; } string key = sender.ToString(CultureInfo.InvariantCulture) + ":" + id; byte[] array = Digest(action, payload, issued); ExpireCache(DateTime.UtcNow.Ticks); string authority; string subject; if (_cache.TryGetValue(key, out var value)) { if (!Fixed(value.RequestDigest, array)) { SendResponse(sender, id, accepted: false, "Administrator request identity was reused.", Array.Empty<byte>()); } else { SendResponse(sender, id, value.Accepted, value.Reason, value.Payload); } } else if (!SentinelTransportIdentity.TryResolvePeer(peer, out authority, out subject)) { SendResponse(sender, id, accepted: false, "Authenticated backend identity is unavailable.", Array.Empty<byte>()); } else { Execute(authority, subject, action, payload, out var accepted, out var response, out var reason); Cache(key, array, accepted, reason, response); SendResponse(sender, id, accepted, reason, response); } } private void Execute(string authority, string subject, string action, byte[] payload, out bool accepted, out byte[] response, out string reason) { accepted = false; response = Array.Empty<byte>(); reason = "Administrator access denied."; if (_runtime.IsBanned(authority, subject)) { reason = "This account is banned."; } else { if (!_runtime.IsAdministrator(authority, subject)) { return; } try { SentinelAdminDocument value; if (action == "status" && payload.Length == 0) { response = SentinelAdminProtocol.Encode(_managed.CreateDocument("Authenticated by " + authority + " backend identity.")); } else if (action == "apply" && SentinelAdminProtocol.TryDecode(payload, out value)) { response = SentinelAdminProtocol.EncodeMessage(_managed.Apply(value, authority, subject)); } else { if (!(action == "tool") || !SentinelAdminProtocol.TryDecodeTool(payload, out var tool)) { reason = "Administrator operation is invalid."; return; } response = SentinelAdminProtocol.EncodeMessage(RunToolCore(tool)); } accepted = true; reason = "ok"; } catch (Exception ex) { reason = Bounded(ex.Message); } } } private string RunToolCore(string tool) { return tool switch { "report" => "Support report created: " + _commands.WriteReport(), "networks" => "Network map created: " + _commands.WriteReport(includeNetworks: true), "backup" => "Verified backup created: " + SentinelTransitionBackup.CreateVerifiedBackupNow("runic-sentinel-admin-tool"), _ => throw new InvalidOperationException("Unknown administrator tool."), }; } private void ReceiveResponse(long sender, ZPackage package) { ZNet instance = ZNet.instance; if (_disposed || (Object)(object)instance == (Object)null || instance.IsServer()) { return; } ZNetPeer serverPeer = instance.GetServerPeer(); if (serverPeer != null && serverPeer.IsReady() && serverPeer.m_uid == sender && TryReadResponse(package, out var id, out var accepted, out var reason, out var payload) && _pending.TryGetValue(id, out var value)) { _pending.Remove(id); if (value.Status != null) { SentinelAdminDocument sentinelAdminDocument = (accepted ? DecodeDocument(payload) : null); value.Status(accepted && sentinelAdminDocument != null, sentinelAdminDocument, (accepted && sentinelAdminDocument == null) ? "Server returned an invalid document." : reason); } else { value.Result?.Invoke(accepted, accepted ? SentinelAdminProtocol.DecodeMessage(payload) : reason); } } } private static SentinelAdminDocument DecodeDocument(byte[] payload) { if (!SentinelAdminProtocol.TryDecode(payload, out var value)) { return null; } return value; } private static ZPackage WriteRequest(string id, string action, byte[] payload, long issued) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(1); val.Write(id); val.Write(action); val.Write(issued); val.Write(payload ?? Array.Empty<byte>()); val.Write(1369914905); return val; } private static bool TryReadRequest(ZPackage package, out string id, out string action, out byte[] payload, out long issued) { id = (action = string.Empty); payload = null; issued = 0L; try { if (package == null || package.Size() < 1 || package.Size() > 184320 || package.ReadInt() != 1) { return false; } id = package.ReadString(); action = package.ReadString(); issued = package.ReadLong(); payload = package.ReadByteArray(); return CanonicalId(id) && (action == "status" || action == "apply" || action == "tool") && payload != null && payload.Length <= 122880 && package.ReadInt() == 1369914905 && package.GetPos() == package.Size(); } catch { return false; } } private static void SendResponse(long peer, string id, bool accepted, string reason, byte[] payload) { ZPackage val = WriteResponse(id, accepted, reason, payload); if (val.Size() <= 184320) { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null) { instance.InvokeRoutedRPC(peer, "runic.sentinel.admin.response.v1", new object[1] { val }); } } } private static ZPackage WriteResponse(string id, bool accepted, string reason, byte[] payload) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(1); val.Write(id ?? string.Empty); val.Write(accepted); val.Write(Bounded(reason)); val.Write(payload ?? Array.Empty<byte>()); val.Write(1369914905); return val; } private static bool TryReadResponse(ZPackage package, out string id, out bool accepted, out string reason, out byte[] payload) { id = (reason = string.Empty); accepted = false; payload = null; try { if (package == null || package.Size() < 1 || package.Size() > 184320 || package.ReadInt() != 1) { return false; } id = package.ReadString(); accepted = package.ReadBool(); reason = package.ReadString(); payload = package.ReadByteArray(); return CanonicalId(id) && reason.Length <= 512 && payload != null && payload.Length <= 122880 && package.ReadInt() == 1369914905 && package.GetPos() == package.Size(); } catch { return false; } } private void Cache(string key, byte[] digest, bool accepted, string reason, byte[] payload) { while (_cache.Count >= 256 && _cacheOrder.Count != 0) { _cache.Remove(_cacheOrder.Dequeue()); } _cache[key] = new CachedResponse { RequestDigest = digest, Accepted = accepted, Reason = Bounded(reason), Payload = (byte[])(payload ?? Array.Empty<byte>()).Clone(), Expires = DateTime.UtcNow.Ticks + ReplayLifetimeTicks }; _cacheOrder.Enqueue(key); } private void ExpireCache(long now) { while (_cacheOrder.Count != 0) { string key = _cacheOrder.Peek(); if (!_cache.TryGetValue(key, out var value) || value.Expires <= now) { _cacheOrder.Dequeue(); _cache.Remove(key); continue; } break; } } private static byte[] Digest(string action, byte[] payload, long issued) { byte[] bytes = Encoding.UTF8.GetBytes(action + "\n" + issued.ToString(CultureInfo.InvariantCulture) + "\n"); byte[] array = new byte[bytes.Length + payload.Length]; Buffer.BlockCopy(bytes, 0, array, 0, bytes.Length); Buffer.BlockCopy(payload, 0, array, bytes.Length, payload.Length); using SHA256 sHA = SHA256.Create(); return sHA.ComputeHash(array); } private static bool Fixed(byte[] left, byte[] right) { if (left != null && right != null && left.Length == right.Length) { return CryptographicOperations.FixedTimeEquals(left, right); } return false; } private static bool CanonicalId(string id) { Guid result; if (id != null && id.Length == 32) { return Guid.TryParseExact(id, "N", out result); } return false; } private static string Bounded(string reason) { if (!string.IsNullOrWhiteSpace(reason)) { if (reason.Length > 512) { return reason.Substring(0, 512); } return reason; } return "Administrator operation failed."; } private static void Fail(Pending pending, string reason) { pending?.Status?.Invoke(arg1: false, null, reason); pending?.Result?.Invoke(arg1: false, reason); } public void Dispose() { _disposed = true; foreach (Pending value in _pending.Values) { Fail(value, "Administrator control stopped."); } _pending.Clear(); _cache.Clear(); _cacheOrder.Clear(); _registeredRpc = null; } } internal sealed class SentinelAdminPanel : IDisposable { private static SentinelAdminPanel _active; private readonly object _callbackGate = new object(); private readonly Queue<Action> _callbacks = new Queue<Action>(); private readonly SentinelAdminControl _control; private Rect _window = new Rect(0f, 0f, 1040f, 740f); private Vector2 _scroll; private SentinelAdminDocument _document; private bool _open; private bool _requesting; private bool _cursorVisible; private CursorLockMode _cursorLock; private int _tab; private string _status = "Press F3 to authenticate with the server."; private GUIStyle _heading; private GUIStyle _section; private GUIStyle _statusStyle; private GUIStyle _textArea; internal static bool IsOpen { get { if (_active != null) { return _active._open; } return false; } } internal SentinelAdminPanel(SentinelAdminControl control) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) _control = control ?? throw new ArgumentNullException("control"); _active = this; } internal void Tick() { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) DrainCallbacks(); KeyboardShortcut val = (KeyboardShortcut)(((??)SentinelConfig.AdminPanelKey?.Value) ?? new KeyboardShortcut((KeyCode)284, Array.Empty<KeyCode>())); if (((KeyboardShortcut)(ref val)).IsDown()) { if (_open) { Close(); } else { RequestOpen(); } } RenewCursorLease(); } internal void Draw() { //IL_010c: Unknown result type (might be due to invalid IL or missing references) //IL_0118: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Expected O, but got Unknown //IL_0122: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Invalid comparison between Unknown and I4 //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Invalid comparison between Unknown and I4 if (_open && _document != null) { if (Event.current != null && (int)Event.current.type == 4 && (int)Event.current.keyCode == 27) { Event.current.Use(); Close(); return; } RenewCursorLease(); EnsureStyles(); float num = Mathf.Min(1100f, (float)Screen.width - 24f); float num2 = Mathf.Min(780f, (float)Screen.height - 24f); ((Rect)(ref _window)).width = num; ((Rect)(ref _window)).height = num2; ((Rect)(ref _window)).x = Mathf.Clamp(((Rect)(ref _window)).x, 12f, Math.Max(12f, (float)Screen.width - num - 12f)); ((Rect)(ref _window)).y = Mathf.Clamp(((Rect)(ref _window)).y, 12f, Math.Max(12f, (float)Screen.height - num2 - 12f)); _window = GUI.Window(730311, _window, new WindowFunction(DrawWindow), "Runic Sentinel Forge — Server Administrator"); } } internal static void RenewCursorLease() { if (IsOpen) { Cursor.lockState = (CursorLockMode)0; Cursor.visible = true; } } private void RequestOpen() { if (_requesting) { return; } _requesting = true; _status = "Authenticating administrator with the authoritative server…"; _control.RequestStatus(delegate(bool ok, SentinelAdminDocument document, string reason) { Enqueue(delegate { //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) _requesting = false; if (!ok || document == null) { _status = "Access denied: " + reason; Player localPlayer = Player.m_localPlayer; if (localPlayer != null) { ((Character)localPlayer).Message((MessageType)2, "Runic Sentinel administrator access denied.", 0, (Sprite)null); } } else { _document = document; _cursorVisible = Cursor.visible; _cursorLock = Cursor.lockState; ((Rect)(ref _window)).x = ((float)Screen.width - ((Rect)(ref _window)).width) * 0.5f; ((Rect)(ref _window)).y = ((float)Screen.height - ((Rect)(ref _window)).height) * 0.5f; _status = document.Status; _open = true; RenewCursorLease(); } }); }); } private void DrawWindow(int id) { //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_01fa: Unknown result type (might be due to invalid IL or missing references) GUILayout.BeginVertical(Array.Empty<GUILayoutOption>()); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); string[] array = new string[5] { "Status", "Mod Policy", "People", "Enforcement", "Admin Tools" }; for (int i = 0; i < array.Length; i++) { if (GUILayout.Toggle(_tab == i, array[i], GUIStyle.op_Implicit("Button"), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { _tab = i; } } if (GUILayout.Button("Close", (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(72f), GUILayout.Height(30f) })) { Close(); } GUILayout.EndHorizontal(); GUILayout.Space(6f); _scroll = GUILayout.BeginScrollView(_scroll, Array.Empty<GUILayoutOption>()); if (_tab == 0) { DrawStatus(); } else if (_tab == 1) { DrawMods(); } else if (_tab == 2) { DrawPeople(); } else if (_tab == 3) { DrawEnforcement(); } else { DrawTools(); } GUILayout.EndScrollView(); GUILayout.FlexibleSpace(); GUILayout.Label(_status ?? string.Empty, _statusStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(42f) }); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUILayout.Label("Every operation is re-authorized by backend account on the server.", Array.Empty<GUILayoutOption>()); GUILayout.FlexibleSpace(); GUI.enabled = !_requesting && _document.ManagedSigningKey; if (GUILayout.Button("Apply & Sign Policy", (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(175f), GUILayout.Height(34f) })) { Apply(); } GUI.enabled = true; GUILayout.EndHorizontal(); GUILayout.EndVertical(); GUI.DragWindow(new Rect(0f, 0f, ((Rect)(ref _window)).width - 80f, 24f)); } private void DrawStatus() { Header("Raven's Gate status"); Row("Signed profile", _document.Profile); Row("Policy sequence", _document.Sequence.ToString()); Row("Runtime integrity", _document.Integrity); Row("Admission transport", _document.Status); Row("Last admission denial", Empty(_document.LastDenial)); Row("Server-managed signing key", _document.ManagedSigningKey ? "Present" : "Not initialized"); Row("Public-key pin", Empty(_document.SigningKeyPin)); GUILayout.Space(10f); GUILayout.Label("The exact DLL list is admission evidence reported by each client. It does not turn a client into a trusted machine. Gameplay security comes from server-owned authorization and validation in every Runic endpoint.", _section, Array.Empty<GUILayoutOption>()); if (!_document.ManagedSigningKey) { GUILayout.Label("Initialize once from the authoritative server console: runic_sentinel bootstrap steam <your SteamID64>", _statusStyle, Array.Empty<GUILayoutOption>()); } } private void DrawMods() { Header("Signed mod passport"); LabeledField("Profile name", ref _document.Profile); LabeledField("Expires (Unix seconds; 0 = never)", ref _document.ExpiresUnixSeconds); GUILayout.Label("Unknown mods", _section, Array.Empty<GUILayoutOption>()); Choice(ref _document.UnknownMods, "Forbidden", "Quarantined", "Unmanaged"); PolicyArea("Required / whitelist — id|version|sha256", ref _document.RequiredMods); PolicyArea("Approved optional — id|version|sha256", ref _document.OptionalMods); PolicyArea("Gray list / unmanaged — id|version|sha256", ref _document.GrayListMods); PolicyArea("Forbidden — id|version|sha256", ref _document.ForbiddenMods); GUILayout.Label("Detected server profile (read-only)", _section, Array.Empty<GUILayoutOption>()); GUILayout.TextArea(_document.DetectedProfile, _textArea, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.MinHeight(150f) }); GUILayout.Label("Standalone transport (server-owned, read-only)", _section, Array.Empty<GUILayoutOption>()); GUILayout.TextArea(_document.Modules, _textArea, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.MinHeight(120f) }); } private void DrawPeople() { Header("Signed identities"); GUILayout.Label("Administrators — authority|subject", _section, Array.Empty<GUILayoutOption>()); GUILayout.Label("Only these authenticated backend accounts may open or use this panel.", Array.Empty<GUILayoutOption>()); _document.Administrators = GUILayout.TextArea(_document.Administrators, _textArea, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.MinHeight(220f) }); GUILayout.Space(12f); GUILayout.Label("Banned users — authority|subject", _section, Array.Empty<GUILayoutOption>()); _document.BannedUsers = GUILayout.TextArea(_document.BannedUsers, _textArea, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.MinHeight(220f) }); } private void DrawEnforcement() { Header("Automatic enforcement"); GUILayout.Label("Admission mode", _section, Array.Empty<GUILayoutOption>()); Choice(ref _document.AdmissionMode, "Required", "Optional", "Disabled"); LabeledField("Runtime DLL/policy check interval (5–300 seconds)", ref _document.IntegritySeconds); LabeledField("Very-high-confidence findings before disconnect (1–10)", ref _document.VeryHighThreshold); LabeledField("High-confidence findings before disconnect (1–20)", ref _document.HighThreshold); LabeledField("Graduated-enforcement window (10–600 seconds)", ref _document.EnforcementWindowSeconds); _document.BackupTransitions = GUILayout.Toggle(_document.BackupTransitions, " Require a verified world backup before policy/modpack transitions", Array.Empty<GUILayoutOption>()); GUILayout.Space(14f); GUILayout.Label("Conclusive violations disconnect immediately. Lesser findings are blocked first and disconnect only after the configured threshold. All decisions are recorded in the bounded security flight recorder.", _section, Array.Empty<GUILayoutOption>()); } private void DrawTools() { Header("Server-owned administrator tools"); Tool("Create Support Report", "report", "Writes bounded policy, profile, integrity, and enforcement evidence."); Tool("Create Production/Portal Network Map", "networks", "Writes the administrator-only live network topology snapshot on the server."); Tool("Create Verified World Backup", "backup", "Creates and validates a Runic Safety backup of the currently loaded world."); } private void Tool(string label, string tool, string explanation) { GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUI.enabled = !_requesting; if (GUILayout.Button(label, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(280f), GUILayout.Height(38f) })) { RunTool(tool); } GUI.enabled = true; GUILayout.Label(explanation, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandWidth(true) }); GUILayout.EndHorizontal(); GUILayout.Space(8f); } private void Apply() { _requesting = true; _status = "Validating, backing up, signing, and applying on the server…"; _control.Apply(_document, delegate(bool ok, string message) { Enqueue(delegate { _requesting = false; _status = (ok ? "Success: " : "Rejected: ") + message; if (ok) { Refresh(); } }); }); } private void RunTool(string tool) { _requesting = true; _status = "Running server tool…"; _control.RunTool(tool, delegate(bool ok, string message) { Enqueue(delegate { _requesting = false; _status = (ok ? "Success: " : "Failed: ") + message; }); }); } private void Refresh() { _requesting = true; _control.RequestStatus(delegate(bool ok, SentinelAdminDocument document, string reason) { Enqueue(delegate { _requesting = false; if (ok && document != null) { _document = document; } else { _status = "Refresh failed: " + reason; } }); }); } private void Close() { //IL_001c: Unknown result type (might be due to invalid IL or missing references) if (_open) { _open = false; Cursor.visible = _cursorVisible; Cursor.lockState = _cursorLock; } } private void EnsureStyles() { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Expected O, but got Unknown //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Expected O, but got Unknown //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Expected O, but got Unknown //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Expected O, but got Unknown if (_heading == null) { _heading = new GUIStyle(GUI.skin.label) { fontSize = 21, fontStyle = (FontStyle)1 }; _heading.normal.textColor = new Color(1f, 0.63f, 0.08f); _section = new GUIStyle(GUI.skin.label) { fontSize = 15, fontStyle = (FontStyle)1, wordWrap = true }; _section.normal.textColor = new Color(1f, 0.72f, 0.25f); _statusStyle = new GUIStyle(GUI.skin.box) { alignment = (TextAnchor)3, wordWrap = true }; _textArea = new GUIStyle(GUI.skin.textArea) { wordWrap = false, fontSize = 13 }; } } private void Header(string text) { GUILayout.Label(text, _heading, Array.Empty<GUILayoutOption>()); GUILayout.Space(8f); } private static void Row(string name, string value) { GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUILayout.Label(name, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(220f) }); GUILayout.Label(value ?? string.Empty, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(22f) }); GUILayout.EndHorizontal(); } private void LabeledField(string label, ref string value) { GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUILayout.Label(label, _section, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(430f) }); value = GUILayout.TextField(value ?? string.Empty, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandWidth(true) }); GUILayout.EndHorizontal(); } private void PolicyArea(string label, ref string value) { GUILayout.Label(label, _section, Array.Empty<GUILayoutOption>()); value = GUILayout.TextArea(value ?? string.Empty, _textArea, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.MinHeight(145f) }); } private static void Choice(ref string value, params string[] choices) { GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); foreach (string text in choices) { if (GUILayout.Toggle(value == text, text, GUIStyle.op_Implicit("Button"), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(150f) })) { value = text; } } GUILayout.EndHorizontal(); } private static string Empty(string value) { if (!string.IsNullOrEmpty(value)) { return value; } return "None"; } private void Enqueue(Action action) { lock (_callbackGate) { _callbacks.Enqueue(action); } } private void DrainCallbacks() { while (true) { Action action; lock (_callbackGate) { if (_callbacks.Count == 0) { break; } action = _callbacks.Dequeue(); } try { action(); } catch { } } } public void Dispose() { Close(); if (_active == this) { _active = null; } lock (_callbackGate) { _callbacks.Clear(); } } } [HarmonyPatch(typeof(GameCamera), "UpdateMouseCapture")] internal static class SentinelAdminCursorPatch { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix() { SentinelAdminPanel.RenewCursorLease(); } } [HarmonyPatch(typeof(Player), "TakeInput")] internal static class SentinelAdminInputPatch { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(Player __instance, ref bool __result) { if ((Object)(object)__instance == (Object)(object)Player.m_localPlayer && SentinelAdminPanel.IsOpen) { __result = false; } } } internal sealed class SentinelAdminDocument { internal long Sequence; internal string Profile = "runic-suite"; internal string ExpiresUnixSeconds = "0"; internal string UnknownMods = "Forbidden"; internal string RequiredMods = string.Empty; internal string OptionalMods = string.Empty; internal string GrayListMods = string.Empty; internal string ForbiddenMods = string.Empty; internal string Administrators = string.Empty; internal string BannedUsers = string.Empty; internal string Modules = string.Empty; internal string DetectedProfile = string.Empty; internal string Integrity = string.Empty; internal string LastDenial = string.Empty; internal string AdmissionMode = "Optional"; internal string IntegritySeconds = "15"; internal string VeryHighThreshold = "2"; internal string HighThreshold = "3"; internal string EnforcementWindowSeconds = "60"; internal bool BackupTransitions = true; internal bool ManagedSigningKey; internal string SigningKeyPin = string.Empty; internal string Status = string.Empty; } internal static class SentinelAdminProtocol { internal const int MaximumWireBytes = 122880; private const string Header = "RUNIC-SENTINEL-ADMIN/1\n"; private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); internal static byte[] Encode(SentinelAdminDocument value) { if (value == null) { throw new ArgumentNullException("value"); } Dictionary<string, string> obj = new Dictionary<string, string>(StringComparer.Ordinal) { ["sequence"] = value.Sequence.ToString(CultureInfo.InvariantCulture), ["profile"] = value.Profile, ["expires"] = value.ExpiresUnixSeconds, ["unknown"] = value.UnknownMods, ["required"] = value.RequiredMods, ["optional"] = value.OptionalMods, ["gray"] = value.GrayListMods, ["forbidden"] = value.ForbiddenMods, ["admins"] = value.Administrators, ["bans"] = value.BannedUsers, ["modules"] = value.Modules, ["detected"] = value.DetectedProfile, ["integrity"] = value.Integrity, ["last-denial"] = value.LastDenial, ["admission"] = value.AdmissionMode, ["integrity-seconds"] = value.IntegritySeconds, ["very-high"] = value.VeryHighThreshold, ["high"] = value.HighThreshold, ["window"] = value.EnforcementWindowSeconds, ["backup"] = (value.BackupTransitions ? "1" : "0"), ["managed-key"] = (value.ManagedSigningKey ? "1" : "0"), ["key-pin"] = value.SigningKeyPin, ["status"] = value.Status }; StringBuilder stringBuilder = new StringBuilder("RUNIC-SENTINEL-ADMIN/1\n"); foreach (KeyValuePair<string, string> item in obj) { stringBuilder.Append(item.Key).Append('=').Append(Convert.ToBase64String(StrictUtf8.GetBytes(item.Value ?? string.Empty))) .Append('\n'); } byte[] bytes = StrictUtf8.GetBytes(stringBuilder.ToString()); if (bytes.Length > 122880) { throw new InvalidDataException("admin-document-too-large"); } return bytes; } internal static bool TryDecode(byte[] bytes, out SentinelAdminDocument value) { value = null; if (bytes == null || bytes.Length == 0 || bytes.Length > 122880) { return false; } string text; try { text = StrictUtf8.GetString(bytes); } catch { return false; } if (!text.StartsWith("RUNIC-SENTINEL-ADMIN/1\n", StringComparison.Ordinal) || text.IndexOf('\r') >= 0 || !text.EndsWith("\n", StringComparison.Ordinal)) { return false; } string[] array = text.Split('\n'); Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal); for (int i = 1; i < array.Length - 1; i++) { int num = array[i].IndexOf('='); if (num <= 0 || !dictionary.TryAdd(array[i].Substring(0, num), Decode(array[i].Substring(num + 1)))) { return false; } } if (!TryLong(dictionary, "sequence", out var result)) { return false; } value = new SentinelAdminDocument { Sequence = result, Profile = Get(dictionary, "profile"), ExpiresUnixSeconds = Get(dictionary, "expires"), UnknownMods = Get(dictionary, "unknown"), RequiredMods = Get(dictionary, "required"), OptionalMods = Get(dictionary, "optional"), GrayListMods = Get(dictionary, "gray"), ForbiddenMods = Get(dictionary, "forbidden"), Administrators = Get(dictionary, "admins"), BannedUsers = Get(dictionary, "bans"), Modules = Get(dictionary, "modules"), DetectedProfile = Get(dictionary, "detected"), Integrity = Get(dictionary, "integrity"), LastDenial = Get(dictionary, "last-denial"), AdmissionMode = Get(dictionary, "admission"), IntegritySeconds = Get(dictionary, "integrity-seconds"), VeryHighThreshold = Get(dictionary, "very-high"), HighThreshold = Get(dictionary, "high"), EnforcementWindowSeconds = Get(dictionary, "window"), BackupTransitions = (Get(dictionary, "backup") == "1"), ManagedSigningKey = (Get(dictionary, "managed-key") == "1"), SigningKeyPin = Get(dictionary, "key-pin"), Status = Get(dictionary, "status") }; return true; } internal static byte[] EncodeTool(string tool) { string text = tool ?? string.Empty; if (text != "report" && text != "networks" && text != "backup") { throw new ArgumentException("Unknown admin tool.", "tool"); } return StrictUtf8.GetBytes("RUNIC-SENTINEL-ADMIN-TOOL/1\n" + text + "\n"); } internal static bool TryDecodeTool(byte[] bytes, out string tool) { tool = string.Empty; if (bytes == null || bytes.Length > 128) { return false; } string text; try { text = StrictUtf8.GetString(bytes); } catch { return false; } if (!text.StartsWith("RUNIC-SENTINEL-ADMIN-TOOL/1\n", StringComparison.Ordinal) || !text.EndsWith("\n", StringComparison.Ordinal)) { return false; } tool = text.Substring("RUNIC-SENTINEL-ADMIN-TOOL/1\n".Length, text.Length - "RUNIC-SENTINEL-ADMIN-TOOL/1\n".Length - 1); if (!(tool == "report") && !(tool == "networks")) { return tool == "backup"; } return true; } internal static byte[] EncodeMessage(string value) { byte[] bytes = StrictUtf8.GetBytes(value ?? string.Empty); if (bytes.Length > 4096) { throw new InvalidDataException("admin-message-too-large"); } return bytes; } internal static string DecodeMessage(byte[] bytes) { if (bytes == null || bytes.Length > 4096) { return "invalid-response"; } try { return StrictUtf8.GetString(bytes); } catch { return "invalid-response"; } } private static string Decode(string value) { try { return StrictUtf8.GetString(Convert.FromBase64String(value)); } catch { return null; } } private static string Get(IDictionary<string, string> values, string key) { if (!values.TryGetValue(key, out var value) || value == null) { return string.Empty; } return value; } private static bool TryLong(IDictionary<string, string> values, string key, out long result) { if (long.TryParse(Get(values, key), NumberStyles.None, CultureInfo.InvariantCulture, out result)) { return result >= 0; } return false; } } internal static class SentinelDraftExporter { internal const string FileName = "RunicSentinel.current-profile.json"; internal static void TryWrite(string configRoot, AttestationSnapshot snapshot) { try { if (snapshot != null && !string.IsNullOrEmpty(configRoot)) { string text = Path.Combine(Path.GetFullPath(configRoot), "RunicSentinel.current-profile.json"); string text2 = text + ".tmp"; byte[] bytes = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetBytes(Build(snapshot)); using (FileStream fileStream = new FileStream(text2, FileMode.Create, FileAccess.Write, FileShare.None, 65536, FileOptions.WriteThrough)) { fileStream.Write(bytes, 0, bytes.Length); fileStream.Flush(flushToDisk: true); } if (File.Exists(text)) { File.Replace(text2, text, null); } else { File.Move(text2, text); } } } catch { } } private static string Build(AttestationSnapshot snapshot) { StringBuilder stringBuilder = new StringBuilder(4096 + snapshot.Plugins.Count * 160); stringBuilder.Append("{\n \"profile\": \"runic-suite\",\n \"sequence\": 1,\n \"issued\": ").Append(DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture)).Append(",\n \"expires\": 0,\n \"unknownMods\": \"Forbidden\",\n") .Append(" \"requiredMods\": [\n"); for (int i = 0; i < snapshot.Plugins.Count; i++) { AttestedPlugin attestedPlugin = snapshot.Plugins[i]; stringBuilder.Append(" { \"id\": \"").Append(Json(attestedPlugin.Id)).Append("\", \"version\": \"") .Append(Json(attestedPlugin.Version)) .Append("\", \"sha256\": \"") .Append(attestedPlugin.Sha256) .Append("\" }") .Append((i + 1 == snapshot.Plugins.Count) ? "\n" : ",\n"); } stringBuilder.Append(" ],\n \"optionalMods\": [],\n \"grayListMods\": [],\n").Append(" \"forbiddenMods\": [],\n \"modules\": [],\n").Append(" \"administrators\": [],\n \"bannedUsers\": []\n}\n"); return stringBuilder.ToString(); } private static string Json(string value) { StringBuilder stringBuilder = new StringBuilder(value?.Length ?? 0); string text = value ?? string.Empty; foreach (char c in text) { switch (c) { case '\\': stringBuilder.Append("\\\\"); continue; case '"': stringBuilder.Append("\\\""); continue; case '\b': stringBuilder.Append("\\b"); continue; case '\f': stringBuilder.Append("\\f"); continue; case '\n': stringBuilder.Append("\\n"); continue; case '\r': stringBuilder.Append("\\r"); continue; case '\t': stringBuilder.Append("\\t"); continue; } if (c < ' ') { StringBuilder stringBuilder2 = stringBuilder.Append("\\u"); int num = c; stringBuilder2.Append(num.ToString("x4")); } else { stringBuilder.Append(c); } } return stringBuilder.ToString(); } } internal sealed class SentinelEnforcementRuntime : IDisposable { private sealed class EscalationState { internal long Started { get; } internal int High { get; set; } internal int VeryHigh { get; set; } internal EscalationState(long started) { Started = started; } } private const int MaximumTrackedPeers = 256; private readonly object _gate = new object(); private readonly SentinelRuntime _runtime; private readonly ISentinelEvidenceProviderLease _evidence; private readonly Dictionary<long, EscalationState> _states = new Dictionary<long, EscalationState>(); private bool _disposed; internal SentinelEnforcementRuntime(SentinelRuntime runtime) { _runtime = runtime ?? throw new ArgumentNullException("runtime"); _evidence = runtime.Evidence.RegisterProvider("runic.sentinel.enforcement"); } internal bool ReportRejectedServerRequest(string sourceModuleId, long peerId, string actor, string rule, string correlationId, FindingConfidence confidence, string detail) { if (sourceModuleId != "runic.portals" || peerId == 0L || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return false; } bool flag = false; long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); lock (_gate) { if (_disposed) { return false; } Prune(num); long num2 = Math.Max(10, Math.Min(600, SentinelConfig.EnforcementWindowSeconds?.Value ?? 60)); int num3 = Math.Max(1, Math.Min(10, SentinelConfig.VeryHighDisconnectCount?.Value ?? 2)); int num4 = Math.Max(1, Math.Min(20, SentinelConfig.HighDisconnectCount?.Value ?? 3)); if (!_states.TryGetValue(peerId, out var value) || num - value.Started > num2) { value = new EscalationState(num); } if (confidence >= FindingConfidence.High) { value.High++; } if (confidence >= FindingConfidence.VeryHigh) { value.VeryHigh++; } flag = confidence == FindingConfidence.Conclusive || value.VeryHigh >= num3 || value.High >= num4; _states[peerId] = value; _evidence.Sink.TryAppend(Safe(actor, "peer:" + peerId), Safe(rule, "security-violation"), Safe(correlationId, Guid.NewGuid().ToString("N")), confidence, flag ? EnforcementAction.Disconnect : EnforcementAction.Cancel, Safe(detail, "request-denied"), out var _); } if (!flag) { return true; } try { ZNetPeer peer = ZNet.instance.GetPeer(peerId); if (peer != null && peer.m_uid == peerId && peer.IsReady()) { ZNet.instance.Disconnect(peer); } } catch { } return true; } private void Prune(long now) { long num = Math.Max(10, Math.Min(600, SentinelConfig.EnforcementWindowSeconds?.Value ?? 60)); List<long> list = new List<long>(); foreach (KeyValuePair<long, EscalationState> state in _states) { if (now - state.Value.Started > num) { list.Add(state.Key); } } foreach (long item in list) { _states.Remove(item); } if (_states.Count < 256) { return; } long key = 0L; long num2 = long.MaxValue; foreach (KeyValuePair<long, EscalationState> state2 in _states) { if (state2.Value.Started < num2) { num2 = state2.Value.Started; key = state2.Key; } } _states.Remove(key); } private static string Safe(string value, string fallback) { string text = (string.IsNullOrEmpty(value) ? fallback : value); if (text.Length > 128) { text = text.Substring(0, 128); } return text; } public void Dispose() { lock (_gate) { if (_disposed) { return; } _disposed = true; _states.Clear(); } try { _evidence.Dispose(); } catch { } } } internal sealed class SentinelFlightRecorder : IDisposable { internal const long MaximumFileBytes = 524288L; private static readonly UTF8Encoding Utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); private static readonly byte[] Header = Utf8.GetBytes("RUNIC-SENTINEL-FLIGHT/1\n"); private readonly object _gate = new object(); private readonly EvidenceLedger _ledger; private readonly ManualLogSource _log; private readonly string _activePath; private readonly string _previousPath; private bool _disposed; private bool _faultLogged; internal string ActivePath => _activePath; internal SentinelFlightRecorder(EvidenceLedger ledger, ManualLogSource log, string configRoot) { _ledger = ledger ?? throw new ArgumentNullException("ledger"); _log = log; string path = Path.Combine(Path.GetFullPath(configRoot), "RunicSentinel", "flight-recorder"); _activePath = Path.Combine(path, "security-current.log"); _previousPath = Path.Combine(path, "security-previous.log"); _ledger.Accepted += OnAccepted; } private void OnAccepted(SecurityEvidence evidence) { if (evidence == null) { return; } byte[] bytes = Utf8.GetBytes(Encode(evidence)); lock (_gate) { if (_disposed) { return; } try { Directory.CreateDirectory(Path.GetDirectoryName(_activePath)); long num = (File.Exists(_activePath) ? new FileInfo(_activePath).Length : 0); long num2 = (long)bytes.Length + (long)((num == 0L) ? Header.Length : 0); if (num + num2 > 524288) { if (File.Exists(_previousPath)) { File.Delete(_previousPath); } if (File.Exists(_activePath)) { File.Move(_activePath, _previousPath); } num = 0L; } using FileStream fileStream = new FileStream(_activePath, FileMode.Append, FileAccess.Write, FileShare.Read, 4096, FileOptions.WriteThrough); if (num == 0L) { fileStream.Write(Header, 0, Header.Length); } fileStream.Write(bytes, 0, bytes.Length); fileStream.Flush(flushToDisk: true); } catch (Exception ex) { if (!_faultLogged) { _faultLogged = true; ManualLogSource log = _log; if (log != null) { log.LogWarning((object)("Sentinel flight-recorder write failed; enforcement remains active: " + ex.GetType().Name + ".")); } } } } } private static string Encode(SecurityEvidence value) { return value.Sequence.ToString(CultureInfo.InvariantCulture) + "|" + value.UnixSeconds.ToString(CultureInfo.InvariantCulture) + "|" + Base64(value.ProviderModuleId) + "|" + Base64(value.Actor) + "|" + Base64(value.Rule) + "|" + Base64(value.CorrelationId) + "|" + ((int)value.Confidence).ToString(CultureInfo.InvariantCulture) + "|" + ((int)value.RequestedAction).ToString(CultureInfo.InvariantCulture) + "|" + ((int)value.EffectiveAction).ToString(CultureInfo.InvariantCulture) + "|" + value.PolicySequence.ToString(CultureInfo.InvariantCulture) + "|" + Base64(value.Detail) + "\n"; } private static string Base64(string value) { return Convert.ToBase64String(Utf8.GetBytes(value ?? string.Empty)); } public void Dispose() { lock (_gate) { if (_disposed) { return; } _disposed = true; } _ledger.Accepted -= OnAccepted; } } internal enum SentinelIntegrityState { Unavailable, MonitorOnly, Ready, Compromised } internal sealed class SentinelIntegritySnapshot { internal SentinelIntegrityState State { get; } internal long CheckedUnixSeconds { get; } internal string ReasonCode { get; } internal string PolicyDigest { get; } internal SentinelIntegritySnapshot(SentinelIntegrityState state, long checkedUnixSeconds, string reasonCode, string policyDigest) { if (!Enum.IsDefined(typeof(SentinelIntegrityState), state) || checkedUnixSeconds < 0) { throw new ArgumentOutOfRangeException("state"); } State = state; CheckedUnixSeconds = checkedUnixSeconds; ReasonCode = (string.IsNullOrEmpty(reasonCode) ? "unavailable" : reasonCode); PolicyDigest = policyDigest ?? string.Empty; } } internal sealed class SentinelManagedPolicyService { private sealed class Rule { internal string Classification { get; } internal string Id { get; } internal string Version { get; } internal string Hash { get; } internal Rule(string classification, string id, string version, string hash) { Classification = classification; Id = id; Version = version; Hash = hash; } } private const string PrivateHeader = "RUNIC-RSA-PRIVATE/1"; private readonly object _gate = new object(); private readonly SentinelRuntime _runtime; private readonly ManualLogSource _log; private readonly string _configRoot; private readonly string _privatePath; private readonly Func<string, string> _backup; internal bool HasManagedKey => File.Exists(_privatePath); internal SentinelManagedPolicyService(SentinelRuntime runtime, ManualLogSource log, string configRoot, Func<string, string> backup) { _runtime = runtime ?? throw new ArgumentNullException("runtime"); _log = log; _configRoot = Path.GetFullPath(configRoot); _privatePath = Path.Combine(_configRoot, "RunicSentinel", "server-private", "RunicSentinel.private.key"); _backup = backup; } internal SentinelAdminDocument CreateDocument(string status = "Ready") { SentinelAdminDocument sentinelAdminDocument = new SentinelAdminDocument { Status = status, AdmissionMode = (SentinelConfig.RemoteAdmissionPolicy?.Value ?? "Optional"), IntegritySeconds = (SentinelConfig.IntegrityCheckSeconds?.Value ?? 15).ToString(CultureInfo.InvariantCulture), VeryHighThreshold = (SentinelConfig.VeryHighDisconnectCount?.Value ?? 2).ToString(CultureInfo.InvariantCulture), HighThreshold = (SentinelConfig.HighDisconnectCount?.Value ?? 3).ToString(CultureInfo.InvariantCulture), EnforcementWindowSeconds = (SentinelConfig.EnforcementWindowSeconds?.Value ?? 60).ToString(CultureInfo.InvariantCulture), BackupTransitions = (SentinelConfig.BackupBeforeTransitions?.Value ?? true), ManagedSigningKey = HasManagedKey, Integrity = _runtime.GetIntegritySnapshot().State.ToString() + ":" + _runtime.GetIntegritySnapshot().ReasonCode, LastDenial = _runtime.LastAdmissionFailure }; if (_runtime.TryGetVerifiedPolicy(out var policy)) { sentinelAdminDocument.Sequence = policy.Sequence; sentinelAdminDocument.Profile = policy.Profile; sentinelAdminDocument.ExpiresUnixSeconds = policy.ExpiresUnixSeconds.ToString(CultureInfo.InvariantCulture); sentinelAdminDocument.UnknownMods = policy.Unknown.ToString(); sentinelAdminDocument.RequiredMods = PluginLines(policy, PluginClassification.Required); sentinelAdminDocument.OptionalMods = PluginLines(policy, PluginClassification.ApprovedOptional); sentinelAdminDocument.GrayListMods = PluginLines(policy, PluginClassification.Unmanaged); sentinelAdminDocument.ForbiddenMods = PluginLines(policy, PluginClassification.Forbidden); sentinelAdminDocument.Administrators = IdentityLines(policy.Administrators); sentinelAdminDocument.BannedUsers = IdentityLines(policy.BannedUsers); sentinelAdminDocument.Modules = "Standalone Sentinel transport; no Runic Core or Runic Persistence dependency."; sentinelAdminDocument.SigningKeyPin = SentinelConfig.TrustedPublicKeySha256?.Value ?? string.Empty; } if (_runtime.TryGetCurrent(out var snapshot, out var status2)) { sentinelAdminDocument.DetectedProfile = string.Join("\n", snapshot.Plugins.Select((AttestedPlugin plugin) => plugin.Id + "|" + plugin.Version + "|" + plugin.Sha256)); } else { sentinelAdminDocument.DetectedProfile = "Snapshot unavailable: " + status2; } return sentinelAdminDocument; } internal string Bootstrap(string authority, string subject) { lock (_gate) { if (!CanonicalAuthority(authority) || !CanonicalSubject(subject)) { throw new InvalidDataException("bootstrap-identity-invalid"); } if (File.Exists(_privatePath)) { throw new InvalidOperationException("managed-signing-key-already-exists"); } if (!_runtime.TryGetCurrent(out var snapshot, out var _)) { throw new InvalidOperationException("sentinel-snapshot-not-ready"); } RSAParameters rSAParameters; using (RSA rSA = RSA.Create()) { rSA.KeySize = 3072; if (rSA.KeySize != 3072) { throw new CryptographicException("rsa-3072-unavailable"); } rSAParameters = rSA.ExportParameters(includePrivateParameters: true); } SentinelPolicy policy; SentinelAdminDocument sentinelAdminDocument = (_runtime.TryGetVerifiedPolicy(out policy) ? CreateDocument("Bootstrap") : DefaultDocument(snapshot)); SortedDictionary<string, SentinelAdministratorRole> sortedDictionary = ParseIdentities(sentinelAdminDocument.Administrators, "administrators"); sortedDictionary[authority + ":" + Uri.EscapeDataString(subject)] = new SentinelAdministratorRole(authority, subject); sentinelAdminDocument.Administrators = IdentityLines(sortedDictionary.Values); string text = ApplyCore(sentinelAdminDocument, null, rSAParameters, changingTrustRoot: true); Directory.CreateDirectory(Path.GetDirectoryName(_privatePath)); try { WriteExclusive(_privatePath, EncodePrivate(rSAParameters)); } catch { throw new IOException("managed-key-persistence-failed-after-policy-signing"); } return text + " Initial administrator: " + authority + ":" + subject + "."; } } internal string Apply(SentinelAdminDocument draft, string callerAuthority, string callerSubject) { if (draft == null) { throw new ArgumentNullException("draft"); } lock (_gate) { if (!File.Exists(_privatePath)) { throw new InvalidOperationException("server-managed-signing-key-required"); } if (!_runtime.TryGetVerifiedPolicy(out var policy)) { throw new InvalidOperationException("verified-policy-required"); } if (draft.Sequence != policy.Sequence) { throw new InvalidOperationException("policy-sequence-stale"); } RSAParameters privateParameters = DecodePrivate(File.ReadAllBytes(_privatePath)); return ApplyCore(draft, callerAuthority + ":" + Uri.EscapeDataString(callerSubject), privateParameters, changingTrustRoot: false); } } private string ApplyCore(SentinelAdminDocument draft, string callerKey, RSAParameters privateParameters, bool changingTrustRoot) { ValidateSettings(draft); SentinelPolicy policy; long num = (_runtime.TryGetVerifiedPolicy(out policy) ? checked(policy.Sequence + 1) : 1); long issued = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); byte[] array = BuildPolicy(draft, num, issued, callerKey); byte[] array2; byte[] array3; string text; using (RSA rSA = RSA.Create()) { rSA.ImportParameters(privateParameters); if (rSA.KeySize != 3072) { throw new CryptographicException("managed-key-not-rsa-3072"); } array2 = rSA.SignData(array, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); array3 = EncodePublic(rSA.ExportParameters(includePrivateParameters: false)); text = Sha256(array3); } if (!PinnedRsaPublicKey.TryParse(array3, text, out var key, out var failure) || !SentinelPolicy.TryParseAndVerify(array, array2, key, out var policy2, out failure)) { throw new InvalidDataException("generated-policy-invalid-" + failure); } if (policy2.Administrators.Count == 0) { throw new InvalidDataException("at-least-one-administrator-required"); } string left = SentinelConfig.TrustedPublicKeySha256?.Value ?? string.Empty; if (!changingTrustRoot && !SentinelPolicy.FixedTimeHexEquals(left, text)) { throw new InvalidOperationException("managed-key-does-not-match-active-trust-root"); } string text2 = _backup?.Invoke("runic-sentinel-admin-policy-apply") ?? "no-world-loaded"; ArchiveCurrent(num); AtomicWrite(Resolve(SentinelConfig.PolicyFile?.Value), array); AtomicWrite(Resolve(SentinelConfig.SignatureFile?.Value), Encoding.ASCII.GetBytes(Convert.ToBase64String(array2) + "\n")); AtomicWrite(Resolve(SentinelConfig.PublicKeyFile?.Value), array3); if (SentinelConfig.TrustedPublicKeySha256 != null && !string.Equals(SentinelConfig.TrustedPublicKeySha256.Value, text, StringComparison.Ordinal)) { SentinelConfig.TrustedPublicKeySha256.Value = text; } ApplySettings(draft); _runtime.Start(_configRoot); ManualLogSource log = _log; if (log != null) { log.LogWarning((object)("Raven's Gate administrator applied signed policy sequence " + num + "; backup=" + text2 + ". Connected clients must receive the public passport before their next strict admission.")); } return "Applied signed policy sequence " + num + ". Backup: " + text2 + ". Public-key pin: " + text + ". Admission-mode changes take effect after restart."; } private byte[] BuildPolicy(SentinelAdminDocument draft, long sequence, long issued, string callerKey) { if (!SentinelPolicy.CanonicalAtom(draft.Profile, 1, 64)) { throw new InvalidDataException("profile-invalid"); } if (!long.TryParse(draft.ExpiresUnixSeconds, NumberStyles.None, CultureInfo.InvariantCulture, out var result) || result < 0 || (result != 0L && result <= issued)) { throw new InvalidDataException("expiration-must-be-zero-or-future-unix-time"); } if (draft.UnknownMods != "Forbidden" && draft.UnknownMods != "Quarantined" && draft.UnknownMods != "Unmanaged") { throw new InvalidDataException("unknown-mod-policy-invalid"); } SortedDictionary<string, Rule> sortedDictionary = new SortedDictionary<string, Rule>(StringComparer.Ordinal); AddRules(draft.RequiredMods, "Required", sortedDictionary); AddRules(draft.OptionalMods, "ApprovedOptional", sortedDictionary); AddRules(draft.GrayListMods, "Unmanaged", sortedDictionary); AddRules(draft.ForbiddenMods, "Forbidden", sortedDictionary); SortedDictionary<string, SentinelAdministratorRole> sortedDictionary2 = ParseIdentities(draft.Administrators, "administrators"); SortedDictionary<string, SentinelAdministratorRole> sortedDictionary3 = ParseIdentities(draft.BannedUsers, "banned-users"); if (sortedDictionary2.Keys.Any(sortedDictionary3.ContainsKey)) { throw new InvalidDataException("identity-cannot-be-admin-and-banned"); } if (sortedDictionary2.Count == 0) { throw new InvalidDataException("at-least-one-administrator-required"); } if (callerKey != null && !sortedDictionary2.ContainsKey(callerKey) && sortedDictionary2.Count < 1) { throw new InvalidDataException("last-administrator-cannot-be-removed"); } IReadOnlyList<SentinelModuleRule> source = Array.Empty<SentinelModuleRule>(); StringBuilder stringBuilder = new StringBuilder(4096); stringBuilder.Append("RUNIC-SENTINEL/3\nprofile=").Append(draft.Profile).Append("\nsequence=") .Append(sequence.ToString(CultureInfo.InvariantCulture)) .Append("\nissued=") .Append(issued.ToString(CultureInfo.InvariantCulture)) .Append("\nexpires=") .Append(result.ToString(CultureInfo.InvariantCulture)) .Append("\nunknown=") .Append(draft.UnknownMods) .Append("\nunknown-capability=Forbidden\n"); foreach (Rule value in sortedDictionary.Values) { stringBuilder.Append("rule=").Append(value.Classification).Append('|') .Append(value.Id) .Append('|') .Append(value.Version) .Append('|') .Append(value.Hash) .Append('\n'); } foreach (SentinelModuleRule item in source.OrderBy<SentinelModuleRule, string>((SentinelModuleRule value) => value.Id, StringComparer.Ordinal)) { stringBuilder.Append("module=").Append(item.Scope).Append('|') .Append(item.Id) .Append('|') .Append(item.Version) .Append('|') .Append(item.Protocol.ToString(CultureInfo.InvariantCulture)) .Append('|') .Append(string.Join(",", item.Capabilities)) .Append('\n'); } foreach (SentinelAdministratorRole value2 in sortedDictionary2.Values) { stringBuilder.Append("role=").Append(value2.Authority).Append('|') .Append(Uri.EscapeDataString(value2.Subject)) .Append('\n'); } foreach (SentinelAdministratorRole value3 in sortedDictionary3.Values) { stringBuilder.Append("ban=").Append(value3.Authority).Append('|') .Append(Uri.EscapeDataString(value3.Subject)) .Append('\n'); } byte[] bytes = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true).GetBytes(stringBuilder.ToString()); if (bytes.Length > 1048576 || bytes.Length > 122880) { throw new InvalidDataException("policy-exceeds-admin-panel-bound"); } return bytes; } private SentinelAdminDocument DefaultDocument(AttestationSnapshot snapshot) { return new SentinelAdminDocument { Profile = "runic-suite", Sequence = 0L, ExpiresUnixSeconds = "0", UnknownMods = "Forbidden", RequiredMods = string.Join("\n", snapshot.Plugins.Select((AttestedPlugin plugin) => plugin.Id + "|" + plugin.Version + "|" + plugin.Sha256)), Modules = string.Empty, AdmissionMode = (SentinelConfig.RemoteAdmissionPolicy?.Value ?? "Optional"), IntegritySeconds = "15", VeryHighThreshold = "2", HighThreshold = "3", EnforcementWindowSeconds = "60", BackupTransitions = true }; } private static void AddRules(string text, string classification, IDictionary<string, Rule> target) { foreach (string item in Lines(text)) { string[] array = item.Split('|'); if (array.Length != 3 || !SentinelPolicy.CanonicalPluginId(array[0]) || !SentinelPolicy.CanonicalVersionOrWildcard(array[1]) || !SentinelPolicy.CanonicalHashOrWildcard(array[2])) { throw new InvalidDataException("plugin-rule-invalid-" + item); } if (target.ContainsKey(array[0])) { throw new InvalidDataException("plugin-listed-more-than-once-" + array[0]); } target.Add(array[0], new Rule(classification, array[0], array[1], array[2])); } } private static SortedDictionary<string, SentinelAdministratorRole> ParseIdentities(string text, string label) { SortedDictionary<string, SentinelAdministratorRole> sortedDictionary = new SortedDictionary<string, SentinelAdministratorRole>(StringComparer.Ordinal); foreach (string item in Lines(text)) { string[] array = item.Split('|'); if (array.Length != 2 || !CanonicalAuthority(array[0]) || !CanonicalSubject(array[1])) { throw new InvalidDataException(label + "-identity-invalid-" + item); } string text2 = array[0] + ":" + Uri.EscapeDataString(array[1]); if (sortedDictionary.ContainsKey(text2)) { throw new InvalidDataException(label + "-duplicate-" + text2); } sortedDictionary.Add(text2, new SentinelAdministratorRole(array[0], array[1])); } return sortedDictionary; } private void ApplySettings(SentinelAdminDocument value) { int value2 = BoundedInt(value.IntegritySeconds, 5, 300, "integrity-seconds"); int value3 = BoundedInt(value.VeryHighThreshold, 1, 10, "very-high-threshold"); int value4 = BoundedInt(value.HighThreshold, 1, 20, "high-threshold"); int value5 = BoundedInt(value.EnforcementWindowSeconds, 10, 600, "enforcement-window"); if (value.AdmissionMode != "Disabled" && value.AdmissionMode != "Optional" && value.AdmissionMode != "Required") { throw new InvalidDataException("admission-mode-invalid"); } Set(SentinelConfig.IntegrityCheckSeconds, value2); Set(SentinelConfig.VeryHighDisconnectCount, value3); Set(SentinelConfig.HighDisconnectCount, value4); Set(SentinelConfig.EnforcementWindowSeconds, value5); Set(SentinelConfig.BackupBeforeTransitions, value.BackupTransitions); Set(SentinelConfig.RemoteAdmissionPolicy, value.AdmissionMode); } private static void ValidateSettings(SentinelAdminDocument value) { BoundedInt(value.IntegritySeconds, 5, 300, "integrity-seconds"); BoundedInt(value.VeryHighThreshold, 1, 10, "very-high-threshold"); BoundedInt(value.HighThreshold, 1, 20, "high-threshold"); BoundedInt(value.EnforcementWindowSeconds, 10, 600, "enforcement-window"); if (value.AdmissionMode != "Disabled" && value.AdmissionMode != "Optional" && value.AdmissionMode != "Required") { throw new InvalidDataException("admission-mode-invalid"); } } private void ArchiveCurrent(long nextSequence) { string text = Path.Combine(_configRoot, "RunicSentinel", "policy-history", "before-sequence-" + nextSequence.ToString(CultureInfo.InvariantCulture)); Directory.CreateDirectory(text); CopyIfPresent(Resolve(SentinelConfig.PolicyFile?.Value), Path.Combine(text, "RunicSentinel.policy")); CopyIfPresent(Resolve(SentinelConfig.SignatureFile?.Value), Path.Combine(text, "RunicSentinel.policy.sig")); CopyIfPresent(Resolve(SentinelConfig.PublicKeyFile?.Value), Path.Combine(text, "RunicSentinel.policy.pub")); } private string Resolve(string configured) { if (!Path.IsPathRooted(configured ?? string.Empty)) { return Path.GetFullPath(Path.Combine(_configRoot, configured ?? string.Empty)); } return Path.GetFullPath(configured); } private static void AtomicWrite(string path, byte[] bytes) { Directory.CreateDirectory(Path.GetDirectoryName(path)); string text = path + ".admin.tmp"; using (FileStream fileStream = new FileStream(text, FileMode.Create, FileAccess.Write, FileShare.None, 65536, FileOptions.WriteThrough)) { fileStream.Write(bytes, 0, bytes.Length); fileStream.Flush(flushToDisk: true); } if (File.Exists(path)) { File.Replace(text, path, null); } else { File.Move(text, path); } } private static void WriteExclusive(string path, byte[] bytes) { using FileStream fileStream = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough); fileStream.Write(bytes, 0, bytes.Length); fileStream.Flush(flushToDisk: true); } private static byte[] EncodePrivate(RSAParameters value) { return Encoding.ASCII.GetBytes("RUNIC-RSA-PRIVATE/1\n" + Text("modulus", value.Modulus) + Text("exponent", value.Exponent) + Text("d", value.D) + Text("p", value.P) + Text("q", value.Q) + Text("dp", value.DP) + Text("dq", value.DQ) + Text("inverseq", value.InverseQ)); static string Text(string name, byte[] bytes) { return name + "=" + Convert.ToBase64String(bytes) + "\n"; } } private static RSAParameters DecodePrivate(byte[] bytes) { if (bytes == null || bytes.Length == 0 || bytes.Length > 65536) { throw new InvalidDataException("managed-key-size-invalid"); } string[] array = Encoding.ASCII.GetString(bytes).Split('\n'); if (array.Length != 10 || array[0] != "RUNIC-RSA-PRIVATE/1" || array[9].Length != 0) { throw new InvalidDataException("managed-key-format-invalid"); } Dictionary<string, byte[]> values = new Dictionary<string, byte[]>(StringComparer.Ordinal); for (int i = 1; i < 9; i++) { int num = array[i].IndexOf('='); if (num <= 0 || values.ContainsKey(array[i].Substring(0, num))) { throw new InvalidDataException("managed-key-field-invalid"); } try { values.Add(array[i].Substring(0, num), Convert.FromBase64String(array[i].Substring(num + 1))); } catch { throw new InvalidDataException("managed-key-base64-invalid"); } } return new RSAParameters { Modulus = Get("modulus"), Exponent = Get("exponent"), D = Get("d"), P = Get("p"), Q = Get("q"), DP = Get("dp"), DQ = Get("dq"), InverseQ = Get("inverseq") }; byte[] Get(string key) { if (!values.TryGetValue(key, out var value) || value.Length == 0) { throw new InvalidDataException("managed-key-field-missing-" + key); } return value; } } private static byte[] EncodePublic(RSAParameters value) { return Encoding.ASCII.GetBytes("RUNIC-RSA-PUBLIC/1\nmodulus=" + Convert.ToBase64String(value.Modulus) + "\nexponent=" + Convert.ToBase64String(value.Exponent) + "\n"); } private static string Sha256(byte[] bytes) { using SHA256 sHA = SHA256.Create(); return SentinelPolicy.Hex(sHA.ComputeHash(bytes)); } private static string PluginLines(SentinelPolicy policy, PluginClassification kind) { return string.Join("\n", from value in policy.Rules where value.Classification == kind select value.Id + "|" + value.Version + "|" + value.Sha256); } private static string IdentityLines(IEnumerable<SentinelAdministratorRole> values) { return string.Join("\n", from value in values.OrderBy<SentinelAdministratorRole, string>((SentinelAdministratorRole value) => value.CanonicalKey, StringComparer.Ordinal) select value.Authority + "|" + value.Subject); } private static string ModuleLines(IEnumerable<SentinelModuleRule> values) { return string.Join("\n", from value in values.OrderBy<SentinelModuleRule, string>((SentinelModuleRule value) => value.Id, StringComparer.Ordinal) select value.Scope.ToString() + "|" + value.Id + "|" + value.Version + "|" + value.Protocol + "|" + string.Join(",", value.Capabilities)); } private static IEnumerable<string> Lines(string value) { return from line in (value ?? string.Empty).Replace("\r", string.Empty).Split('\n') select line.Trim() into line where line.Length > 0 select line; } private static bool CanonicalAuthority(string value) { if (SentinelPolicy.CanonicalAtom(value, 1, 64)) { return value.All((char character) => character < 'A' || character > 'Z'); } return false; } private static bool CanonicalSubject(string value) { if (value != null && value.Length > 0 && value.Length <= 256 && !value.Any(char.IsControl) && !char.IsWhiteSpace(value[0])) { return !char.IsWhiteSpace(value[value.Length - 1]); } return false; } private static int BoundedInt(string value, int minimum, int maximum, string label) { if (!int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out var result) || result < minimum || result > maximum) { throw new InvalidDataException(label + "-invalid"); } return result; } private static void Set<T>(ConfigEntry<T> entry, T value) { if (entry != null && !EqualityComparer<T>.Default.Equals(entry.Value, value)) { entry.Value = value; } } private static void CopyIfPresent(string source, string destination) { if (File.Exists(source) && !File.Exists(destination)) { File.Copy(source, destination, overwrite: false); } } } internal static class SentinelNetworkMapWriter { private const int MaximumZdos = 16384; private const int MaximumEdges = 2048; private static readonly FieldInfo ObjectsField = AccessTools.Field(typeof(ZDOMan), "m_objectsByID"); private static readonly string[] ProductionRoles = new string[4] { "input", "fuel", "output", "replenishment" }; internal static bool TryAppend(StringBuilder builder, out string failure) { //IL_0108: Unknown result type (might be due to invalid IL or missing references) //IL_019d: Unknown result type (might be due to invalid IL or missing references) //IL_022b: Unknown result type (might be due to invalid IL or missing references) //IL_02a2: Unknown result type (might be due to invalid IL or missing references) failure = string.Empty; if (builder == null) { failure = "builder-missing"; return false; } ZNet instance = ZNet.instance; ZDOMan instance2 = ZDOMan.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer() || instance2 == null) { failure = "server-console-required"; return false; } if (!(ObjectsField?.GetValue(instance2) is Dictionary<ZDOID, ZDO> dictionary)) { failure = "world-index-unavailable"; return false; } int val = 0; int num = 0; int num2 = 0; builder.Append("network-map=server-local-snapshot\n"); foreach (KeyValuePair<ZDOID, ZDO> item in dictionary) { if (val++ >= 16384) { break; } ZDO value = item.Value; if (value == null || !value.IsValid()) { continue; } string value2 = value.GetString("runic.portals.record", string.Empty); string value3 = Safe(value.GetString("runic.portals.network", string.Empty), 64); if (!string.IsNullOrEmpty(value2) || !string.IsNullOrEmpty(value3)) { num++; builder.Append("portal=").Append(Id(item.Key)).Append('|') .Append(value3) .Append('|') .Append(Safe(value.GetString("runic.portals.name", string.Empty), 64)) .Append('|') .Append(value.GetInt("runic.portals.networkKind", 0).ToString(CultureInfo.InvariantCulture)) .Append('|') .Append(Safe(value.GetString("runic.portals.group", string.Empty), 64)) .Append('|') .Append(Position(value.GetPosition())) .Append('\n'); } for (int i = 0; i < ProductionRoles.Length; i++) { if (num2 >= 2048) { break; } string text = ProductionRoles[i]; string text2 = value.GetString("runic.production." + text + ".record", string.Empty); if (!string.IsNullOrEmpty(text2) && !(text2 == "!")) { num2++; if (!TryReadProductionTarget(text2, i, out var linkId, out var target)) { target = "record-invalid"; } builder.Append("production-edge=").Append(Id(item.Key)).Append('|') .Append(value.GetPrefab().ToString(CultureInfo.InvariantCulture)) .Append('|') .Append(text) .Append('|') .Append(Safe(linkId, 128)) .Append('|') .Append(Safe(target, 160)) .Append('|') .Append(Position(value.GetPosition())) .Append('\n'); } } } builder.Append("network-map-summary=zdos:").Append(Math.Min(val, 16384).ToString(CultureInfo.InvariantCulture)).Append(",portals:") .Append(num.ToString(CultureInfo.InvariantCulture)) .Append(",production-edges:") .Append(num2.ToString(CultureInfo.InvariantCulture)) .Append(",truncated:") .Append((dictionary.Count > 16384 || num2 >= 2048) ? "true" : "false") .Append('\n'); return true; } private static bool TryReadProductionTarget(string encoded, int expectedRole, out string linkId, out string target) { //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Expected O, but got Unknown //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Expected O, but got Unknown linkId = string.Empty; target = string.Empty; try { if (encoded.Length > 2048) { return false; } byte[] array = Convert.FromBase64String(encoded); if (array.Length == 0 || array.Length > 1536) { return false; } ZPackage val = new ZPackage(array); byte[] array2 = val.ReadByteArray(); byte[] array3 = val.ReadByteArray(); if (val.GetPos() != val.Size() || array2 == null || array2.Length == 0 || array2.Length > 1536 || array3 == null || array3.Length != 32) { return false; } using (SHA256 sHA = SHA256.Create()) { byte[] array4 = sHA.ComputeHash(array2); int num = 0; for (int i = 0; i < array4.Length; i++) { num |= array4[i] ^ array3[i]; } if (num != 0) { return false; } } ZPackage val2 = new ZPackage(array2); if (val2.ReadInt() != 1 || val2.ReadInt() != expectedRole) { return false; } linkId = val2.ReadString(); target = val2.ReadString(); return val2.GetPos() <= val2.Size() && linkId.Length <= 128 && target.Length <= 160; } catch { linkId = string.Empty; target = string.Empty; return false; } } private unsafe static string Id(ZDOID id) { return Safe(((object)(*(ZDOID*)(&id))/*cast due to .constrained prefix*/).ToString(), 96); } private static string Position(Vector3 value) { return value.x.ToString("F1", CultureInfo.InvariantCulture) + "," + value.y.ToString("F1", CultureInfo.InvariantCulture) + "," + value.z.ToString("F1", CultureInfo.InvariantCulture); } private static string Safe(string value, int maximum) { if (string.IsNullOrEmpty(value)) { return string.Empty; } if (value.Length > maximum) { value = value.Substring(0, maximum); } for (int i = 0; i < value.Length; i++) { if (char.IsControl(value[i]) || value[i] == '|') { return "invalid-text"; } } return value; } } internal sealed class SentinelOperatorCommands : IDisposable { private const int MaximumReportBytes = 524288; private readonly SentinelRuntime _runtime; private readonly ManualLogSource _log; private readonly string _reportRoot; private readonly SentinelManagedPolicyService _managed; private readonly ConsoleCommand _command; private bool _disposed; internal SentinelOperatorCommands(SentinelRuntime runtime, ManualLogSource log, string configRoot, SentinelManagedPolicyService managed) { //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Expected O, but got Unknown //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Expected O, but got Unknown _runtime = runtime ?? throw new ArgumentNullException("runtime"); _log = log; _managed = managed ?? throw new ArgumentNullException("managed"); _reportRoot = Path.Combine(Path.GetFullPath(configRoot), "RunicSentinel", "reports"); _command = new ConsoleCommand("runic_sentinel", "Raven's Gate: status | report | networks | bootstrap <authority> <subject>", new ConsoleEvent(OnCommand), false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); } private void OnCommand(ConsoleEventArgs args) { if (_disposed || (Object)(object)args?.Context == (Object)null) { return; } switch ((args.Args.Length > 1) ? args.Args[1].Trim().ToLowerInvariant() : "status") { case "status": { SentinelIntegritySnapshot integritySnapshot = _runtime.GetIntegritySnapshot(); args.Context.AddString("Raven's Gate: " + integritySnapshot.State.ToString() + "; profile=" + ((_runtime.PolicyProfile.Length == 0) ? "none" : _runtime.PolicyProfile) + "; sequence=" + _runtime.PolicySequence.ToString(CultureInfo.InvariantCulture) + "; admission=" + (_runtime.AuthoritativeTransportReady ? "standalone-routed" : "unavailable") + "; last-denial=" + ((_runtime.LastAdmissionFailure.Length == 0) ? "none" : _runtime.LastAdmissionFailure) + "."); break; } case "report": try { string text = WriteReport(); args.Context.AddString("Runic Sentinel support report created: " + text); break; } catch (Exception ex2) { args.Context.AddString("Runic Sentinel report failed closed: " + ex2.GetType().Name + "."); ManualLogSource log = _log; if (log != null) { log.LogWarning((object)("Sentinel report failed: " + ex2.GetType().Name + ".")); } break; } case "networks": try { string text2 = WriteReport(includeNetworks: true); args.Context.AddString("Runic Sentinel administrator network snapshot created: " + text2); break; } catch (Exception ex3) { args.Context.AddString((ex3.Message == "server-console-required") ? "Runic Sentinel network maps are available only on the authoritative server." : ("Runic Sentinel network snapshot failed closed: " + ex3.GetType().Name + ".")); break; } case "bootstrap": if (args.Args.Length != 4) { args.Context.AddString("Usage: runic_sentinel bootstrap <authority> <subject>"); break; } try { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { throw new InvalidOperationException("authoritative-server-console-required"); } args.Context.AddString(_managed.Bootstrap(args.Args[2], args.Args[3])); break; } catch (Exception ex) { args.Context.AddString("Runic Sentinel bootstrap failed closed: " + ex.Message); break; } default: args.Context.AddString("Usage: runic_sentinel status | report | networks | bootstrap <authority> <subject>"); break; } } internal string WriteReport(bool includeNetworks = false) { StringBuilder stringBuilder = new StringBuilder(16384); SentinelIntegritySnapshot integritySnapshot = _runtime.GetIntegritySnapshot(); stringBuilder.Append("RUNIC-SENTINEL-SUPPORT/1\n").Append("created-utc=").Append(DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)) .Append('\n') .Append("integrity=") .Append(integritySnapshot.State) .Append('\n') .Append("integrity-reason=") .Append(integritySnapshot.ReasonCode) .Append('\n') .Append("policy-profile=") .Append(_runtime.PolicyProfile) .Append('\n') .Append("policy-sequence=") .Append(_runtime.PolicySequence.ToString(CultureInfo.InvariantCulture)) .Append('\n') .Append("policy-digest=") .Append(integritySnapshot.PolicyDigest) .Append('\n') .Append("admission-transport=") .Append(_runtime.AuthoritativeTransportReady ? "ready" : "unavailable") .Append('\n') .Append("last-admission-denial=") .Append(_runtime.LastAdmissionFailure) .Append('\n'); if (_runtime.TryGetCurrent(out var snapshot, out var status)) { stringBuilder.Append("snapshot-status=").Append(status).Append('\n') .Append("snapshot-digest=") .Append(snapshot.Digest) .Append('\n') .Append("plugins=") .Append(snapshot.Plugins.Count.ToString(CultureInfo.InvariantCulture)) .Append('\n'); foreach (AttestedPlugin plugin in snapshot.Plugins) { stringBuilder.Append("plugin=").Append(plugin.Id).Append('|') .Append(plugin.Version) .Append('|') .Append(plugin.Sha256) .Append('\n'); } } else { stringBuilder.Append("snapshot-status=").Append(status).Append('\n'); } stringBuilder.Append("transport=standalone-valheim\n"); EvidenceReadSnapshot evidenceReadSnapshot = _runtime.Evidence.ReadAfter(0L, 256); stringBuilder.Append("evidence-newest=").Append(evidenceReadSnapshot.NewestSequence.ToString(CultureInfo.InvariantCulture)).Append('\n'); foreach (SecurityEvidence entry in evidenceReadSnapshot.Entries) { stringBuilder.Append("evidence=").Append(entry.Sequence.ToString(CultureInfo.InvariantCulture)).Append('|') .Append(entry.UnixSeconds.ToString(CultureInfo.InvariantCulture)) .Append('|') .Append(entry.ProviderModuleId) .Append('|') .Append(entry.Rule) .Append('|') .Append(entry.Confidence) .Append('|') .Append(entry.EffectiveAction) .Append('|') .Append(entry.Detail) .Append('\n'); } if (includeNetworks && !SentinelNetworkMapWriter.TryAppend(stringBuilder, out var failure)) { throw new InvalidOperationException(failure); } byte[] bytes = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetBytes(stringBuilder.ToString()); if (bytes.Length > 524288) { throw new InvalidDataException("The bounded support report exceeded 512 KiB."); } Directory.CreateDirectory(_reportRoot); string text = Path.Combine(_reportRoot, "sentinel-report-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture) + ".txt"); using FileStream fileStream = new FileStream(text, FileMode.CreateNew, FileAccess.Write, FileShare.Read, 65536, FileOptions.WriteThrough); fileStream.Write(bytes, 0, bytes.Length); fileStream.Flush(flushToDisk: true); return text; } public void Dispose() { _disposed = true; } } internal sealed class SentinelRuntime : ISentinelAttestationService, ISentinelAdmissionService, ISentinelNetworkProfileSource, IDisposable { private sealed class FileEvidence { internal string Sha256 { get; } internal long Length { get; } internal long LastWriteUtcTicks { get; } internal FileEvidence(string sha256, long length, long lastWriteUtcTicks) { Sha256 = sha256; Length = length; LastWriteUtcTicks = lastWriteUtcTicks; } } private sealed class IntegrityFileStamp { internal string Path { get; } internal long Length { get; } internal long LastWriteUtcTicks { get; } internal bool PolicyAsset { get; } internal IntegrityFileStamp(string path, long length, long lastWriteUtcTicks, bool policyAsset) { Path = path; Length = length; LastWri