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 Enabled; internal static ConfigEntry PolicyFile; internal static ConfigEntry SignatureFile; internal static ConfigEntry PublicKeyFile; internal static ConfigEntry TrustedPublicKeySha256; internal static ConfigEntry RemoteAdmissionPolicy; internal static ConfigEntry IntegrityCheckSeconds; internal static ConfigEntry BackupBeforeTransitions; internal static ConfigEntry VeryHighDisconnectCount; internal static ConfigEntry HighDisconnectCount; internal static ConfigEntry EnforcementWindowSeconds; internal static ConfigEntry 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("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("Policy", "ManifestFile", "RunicSentinel.policy", "Canonical RUNIC-SENTINEL/3 policy path, relative to BepInEx/config unless absolute."); SignatureFile = config.Bind("Policy", "SignatureFile", "RunicSentinel.policy.sig", "Canonical Base64 detached RSA-3072/SHA-256 PKCS#1 v1.5 signature path."); PublicKeyFile = config.Bind("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("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("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("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("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("Automatic Enforcement", "VeryHighFindingsBeforeDisconnect", 2, new ConfigDescription("Disconnect after this many very-high-confidence violations in the enforcement window.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 10), Array.Empty())); HighDisconnectCount = config.Bind("Automatic Enforcement", "HighFindingsBeforeDisconnect", 3, new ConfigDescription("Disconnect after this many high-confidence violations in the enforcement window.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 20), Array.Empty())); EnforcementWindowSeconds = config.Bind("Automatic Enforcement", "FindingWindowSeconds", 60, new ConfigDescription("Rolling violation window used by graduated automatic enforcement.", (AcceptableValueBase)(object)new AcceptableValueRange(10, 600), Array.Empty())); AdminPanelKey = config.Bind("Administrator Panel", "OpenPanel", new KeyboardShortcut((KeyCode)284, Array.Empty()), "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 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 Status; internal Action 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 _pending = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _cache = new Dictionary(StringComparer.Ordinal); private readonly Queue _cacheOrder = new Queue(); 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("runic.sentinel.admin.request.v1", (Action)ReceiveRequest); instance.Register("runic.sentinel.admin.response.v1", (Action)ReceiveResponse); _registeredRpc = instance; _pending.Clear(); } long ticks = DateTime.UtcNow.Ticks; foreach (string item in new List(_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 callback) { if (TryExecuteLocal("status", Array.Empty(), out var accepted, out var response, out var reason)) { callback?.Invoke(accepted, DecodeDocument(response), reason); return; } Submit("status", Array.Empty(), new Pending { Status = callback }); } internal void Apply(SentinelAdminDocument document, Action 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 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(); 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()); return; } long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (issued < num - 30 || issued > num + 30) { SendResponse(sender, id, accepted: false, "Administrator request expired.", Array.Empty()); 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()); } 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()); } 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(); 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()); 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()); 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()).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 _callbacks = new Queue(); 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())); 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()); GUILayout.BeginHorizontal(Array.Empty()); 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()); 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()); GUILayout.Label("Every operation is re-authorized by backend account on the server.", Array.Empty()); 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()); if (!_document.ManagedSigningKey) { GUILayout.Label("Initialize once from the authoritative server console: runic_sentinel bootstrap steam ", _statusStyle, Array.Empty()); } } 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()); 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()); GUILayout.TextArea(_document.DetectedProfile, _textArea, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.MinHeight(150f) }); GUILayout.Label("Standalone transport (server-owned, read-only)", _section, Array.Empty()); 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()); GUILayout.Label("Only these authenticated backend accounts may open or use this panel.", Array.Empty()); _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()); _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()); 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()); 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()); } 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()); 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()); GUILayout.Space(8f); } private static void Row(string name, string value) { GUILayout.BeginHorizontal(Array.Empty()); 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()); 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()); 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()); 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 obj = new Dictionary(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 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 dictionary = new Dictionary(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 values, string key) { if (!values.TryGetValue(key, out var value) || value == null) { return string.Empty; } return value; } private static bool TryLong(IDictionary 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 _states = new Dictionary(); 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 list = new List(); foreach (KeyValuePair 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 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 _backup; internal bool HasManagedKey => File.Exists(_privatePath); internal SentinelManagedPolicyService(SentinelRuntime runtime, ManualLogSource log, string configRoot, Func 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 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 sortedDictionary = new SortedDictionary(StringComparer.Ordinal); AddRules(draft.RequiredMods, "Required", sortedDictionary); AddRules(draft.OptionalMods, "ApprovedOptional", sortedDictionary); AddRules(draft.GrayListMods, "Unmanaged", sortedDictionary); AddRules(draft.ForbiddenMods, "Forbidden", sortedDictionary); SortedDictionary sortedDictionary2 = ParseIdentities(draft.Administrators, "administrators"); SortedDictionary 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 source = Array.Empty(); 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 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 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 ParseIdentities(string text, string label) { SortedDictionary sortedDictionary = new SortedDictionary(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 values = new Dictionary(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 values) { return string.Join("\n", from value in values.OrderBy((SentinelAdministratorRole value) => value.CanonicalKey, StringComparer.Ordinal) select value.Authority + "|" + value.Subject); } private static string ModuleLines(IEnumerable values) { return string.Join("\n", from value in values.OrderBy((SentinelModuleRule value) => value.Id, StringComparer.Ordinal) select value.Scope.ToString() + "|" + value.Id + "|" + value.Version + "|" + value.Protocol + "|" + string.Join(",", value.Capabilities)); } private static IEnumerable 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(ConfigEntry entry, T value) { if (entry != null && !EqualityComparer.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 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 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 ", 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 "); 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 "); 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; LastWriteUtcTicks = lastWriteUtcTicks; PolicyAsset = policyAsset; } } private sealed class WorkerInputs { internal string ConfigRoot { get; } internal string PolicyPath { get; } internal string SignaturePath { get; } internal string PublicKeyPath { get; } internal string PinnedPublicKeySha256 { get; } internal WorkerInputs(string configRoot, string policyPath, string signaturePath, string publicKeyPath, string pinnedPublicKeySha256) { ConfigRoot = configRoot; PolicyPath = policyPath; SignaturePath = signaturePath; PublicKeyPath = publicKeyPath; PinnedPublicKeySha256 = pinnedPublicKeySha256; } } private sealed class PluginDescriptor { internal string Id { get; } internal string Version { get; } internal string Path { get; } internal string[] Dependencies { get; } internal string[] Capabilities { get; } internal PluginDescriptor(string id, string version, string path, string[] dependencies, string[] capabilities) { Id = id; Version = version; Path = path; Dependencies = dependencies; Capabilities = capabilities; } } private const long MaximumPluginBytes = 536870912L; private const long MaximumTotalPluginBytes = 4294967296L; private static readonly StringComparer PathComparer = (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal); private readonly object _gate = new object(); private CancellationTokenSource _cancel; private AttestationSnapshot _snapshot; private SentinelPolicy _policy; private SentinelNetworkProfile _networkProfile; private SentinelNetworkCompatibility _network; private string _lastAdmissionFailure = string.Empty; private IntegrityFileStamp[] _integrityFiles = Array.Empty(); private bool _integrityCompromised; private string _integrityReason = "snapshot-unavailable"; private long _nextIntegrityCheckUtcTicks; private string _status = "NotStarted"; private long _generation; private long _highestPolicySequence; private string _highestPolicyDigest = string.Empty; private bool _disposed; internal EvidenceLedger Evidence { get; } public bool ProvidesClientAuthenticityProof => false; public bool PolicyReady { get { lock (_gate) { return _policy != null; } } } public bool AuthoritativeTransportReady { get { lock (_gate) { return _network != null && _network.IsActive; } } } public long PolicySequence { get { lock (_gate) { return _policy?.Sequence ?? 0; } } } public string PolicyProfile { get { lock (_gate) { return _policy?.Profile ?? string.Empty; } } } internal string LastAdmissionFailure { get { lock (_gate) { return _lastAdmissionFailure; } } } internal SentinelRuntime() { Evidence = new EvidenceLedger(); } internal bool TryGetVerifiedPolicy(out SentinelPolicy policy) { lock (_gate) { policy = (PolicyCurrentLocked() ? _policy : null); return policy != null; } } internal void Start(string configDirectory) { long generation; CancellationTokenSource cancel; CancellationTokenSource cancellationTokenSource; lock (_gate) { if (_disposed) { throw new ObjectDisposedException("SentinelRuntime"); } if (_generation == long.MaxValue) { throw new InvalidOperationException("Sentinel worker generation space is exhausted."); } generation = ++_generation; cancel = _cancel; cancellationTokenSource = (_cancel = new CancellationTokenSource()); _snapshot = null; _policy = null; _networkProfile = null; _integrityFiles = Array.Empty(); _integrityCompromised = false; _integrityReason = "snapshot-pending"; _nextIntegrityCheckUtcTicks = 0L; _status = "PendingLocalSnapshot"; Evidence.SetPolicySequence(0L); } CancelAndDispose(cancel); PluginDescriptor[] descriptors; WorkerInputs inputs; try { descriptors = CaptureDescriptors(); inputs = CaptureInputs(configDirectory); } catch (Exception ex) { PublishFailure(generation, cancellationTokenSource.Token, ex.GetType().Name); CancelAndDispose(cancellationTokenSource); throw; } CancellationToken token = cancellationTokenSource.Token; Task.Run(delegate { Build(generation, descriptors, inputs, token); }, token); } public bool TryGetCurrent(out AttestationSnapshot snapshot, out string status) { lock (_gate) { snapshot = _snapshot; status = _status; return snapshot != null; } } public bool TryComputeNonceBinding(string nonceHex, out string bindingHex, out string status) { lock (_gate) { status = _status + ":UnauthenticatedNonceBinding"; if (_snapshot == null) { bindingHex = string.Empty; return false; } return AttestationPolicy.TryComputeNonceBinding(nonceHex, _snapshot.Digest, out bindingHex); } } public AdmissionDecision Evaluate(AttestationSnapshot snapshot, string role) { lock (_gate) { return AdmissionPolicy.Evaluate(_policy, snapshot, role ?? string.Empty); } } internal void AttachNetwork(SentinelRemoteAdmissionMode mode) { SentinelNetworkCompatibility sentinelNetworkCompatibility = new SentinelNetworkCompatibility(this, Evidence, mode); lock (_gate) { if (_disposed || _network != null) { sentinelNetworkCompatibility.Dispose(); if (_disposed) { throw new ObjectDisposedException("SentinelRuntime"); } throw new InvalidOperationException("Sentinel network compatibility is already attached."); } _network = sentinelNetworkCompatibility; } } internal void TickNetwork() { _network?.Tick(); } internal bool IsAdministrator(string authority, string subject) { lock (_gate) { return PolicyCurrentLocked() && _policy.Administrators.Any((SentinelAdministratorRole role) => string.Equals(role.Authority, authority, StringComparison.Ordinal) && string.Equals(role.Subject, subject, StringComparison.Ordinal)); } } internal bool IsBanned(string authority, string subject) { lock (_gate) { return PolicyCurrentLocked() && _policy.BannedUsers.Any((SentinelAdministratorRole role) => string.Equals(role.Authority, authority, StringComparison.Ordinal) && string.Equals(role.Subject, subject, StringComparison.Ordinal)); } } internal SentinelIntegritySnapshot GetIntegritySnapshot() { lock (_gate) { return new SentinelIntegritySnapshot(_integrityCompromised ? SentinelIntegrityState.Compromised : ((_snapshot != null) ? ((_policy == null) ? SentinelIntegrityState.MonitorOnly : SentinelIntegrityState.Ready) : SentinelIntegrityState.Unavailable), DateTimeOffset.UtcNow.ToUnixTimeSeconds(), _integrityReason, _policy?.PayloadDigest ?? string.Empty); } } internal void TickIntegrity() { long ticks = DateTime.UtcNow.Ticks; IntegrityFileStamp[] integrityFiles; lock (_gate) { if (_disposed || _integrityCompromised || ticks < _nextIntegrityCheckUtcTicks) { return; } int num = Math.Max(5, Math.Min(300, SentinelConfig.IntegrityCheckSeconds?.Value ?? 15)); _nextIntegrityCheckUtcTicks = ticks + TimeSpan.FromSeconds(num).Ticks; if (_policy != null && _policy.ExpiresUnixSeconds != 0L && DateTimeOffset.UtcNow.ToUnixTimeSeconds() >= _policy.ExpiresUnixSeconds) { _integrityCompromised = true; _integrityReason = "passport-expired"; _lastAdmissionFailure = "sentinel-passport-expired"; return; } integrityFiles = _integrityFiles; } foreach (IntegrityFileStamp integrityFileStamp in integrityFiles) { FileInfo fileInfo = new FileInfo(integrityFileStamp.Path); if (!fileInfo.Exists || fileInfo.Length != integrityFileStamp.Length || fileInfo.LastWriteTimeUtc.Ticks != integrityFileStamp.LastWriteUtcTicks) { lock (_gate) { _integrityCompromised = true; _integrityReason = (integrityFileStamp.PolicyAsset ? "passport-file-changed" : "plugin-file-changed"); _lastAdmissionFailure = "sentinel-runtime-integrity-changed"; break; } } } } private bool PolicyCurrentLocked() { if (_policy == null || _integrityCompromised) { return false; } long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (_policy.IssuedUnixSeconds <= num + 300) { if (_policy.ExpiresUnixSeconds != 0L) { return num < _policy.ExpiresUnixSeconds; } return true; } return false; } internal void RecordAdmissionFailure(string reason) { if (string.IsNullOrEmpty(reason) || reason.Length > 128 || !RunicIdentifier.IsValid(reason)) { reason = "sentinel-admission-denied"; } lock (_gate) { _lastAdmissionFailure = reason; } } internal bool TryGetTransitionFingerprint(out string fingerprint) { lock (_gate) { if (_snapshot == null || _policy == null || _integrityCompromised) { fingerprint = string.Empty; return false; } using SHA256 sHA = SHA256.Create(); fingerprint = SentinelPolicy.Hex(sHA.ComputeHash(Encoding.ASCII.GetBytes("RUNIC-TRANSITION/1\n" + _policy.PayloadDigest + "\n" + _snapshot.Digest + "\n"))); return true; } } public bool TryGetNetworkProfile(out SentinelNetworkProfile profile) { lock (_gate) { profile = _networkProfile; return profile != null; } } public void Dispose() { CancellationTokenSource cancel; SentinelNetworkCompatibility network; lock (_gate) { if (_disposed) { return; } _disposed = true; if (_generation < long.MaxValue) { _generation++; } cancel = _cancel; _cancel = null; _snapshot = null; _policy = null; _networkProfile = null; _integrityFiles = Array.Empty(); _integrityCompromised = false; _integrityReason = "disposed"; network = _network; _network = null; _status = "Disposed"; Evidence.SetPolicySequence(0L); } try { network?.Dispose(); } catch (Exception) { } CancelAndDispose(cancel); } private void Build(long generation, PluginDescriptor[] descriptors, WorkerInputs inputs, CancellationToken token) { try { List list = new List(descriptors.Length); Dictionary dictionary = new Dictionary(PathComparer); long num = 0L; byte[] buffer = new byte[65536]; for (int i = 0; i < descriptors.Length; i++) { token.ThrowIfCancellationRequested(); PluginDescriptor pluginDescriptor = descriptors[i]; string fullPath = Path.GetFullPath(pluginDescriptor.Path); if (!dictionary.TryGetValue(fullPath, out var value)) { FileInfo fileInfo = new FileInfo(fullPath); if (!fileInfo.Exists || fileInfo.Length <= 0 || fileInfo.Length > 536870912 || num > 4294967296L - fileInfo.Length) { throw new InvalidDataException("Plugin file bound failed."); } long length = fileInfo.Length; DateTime lastWriteTimeUtc = fileInfo.LastWriteTimeUtc; num += length; string sha = HashStablePlugin(fullPath, length, buffer, token); fileInfo.Refresh(); if (!fileInfo.Exists || fileInfo.Length != length || fileInfo.LastWriteTimeUtc != lastWriteTimeUtc) { throw new IOException("Plugin changed during local snapshot hashing."); } value = new FileEvidence(sha, length, lastWriteTimeUtc.Ticks); dictionary.Add(fullPath, value); } list.Add(new AttestedPlugin(pluginDescriptor.Id, pluginDescriptor.Version, value.Sha256, pluginDescriptor.Dependencies, pluginDescriptor.Capabilities)); } if (!AttestationPolicy.TryCanonicalize(list, out var plugins, out var canonical, out var failure)) { throw new InvalidDataException(failure); } AttestationSnapshot snapshot = new AttestationSnapshot(AttestationPolicy.Digest(canonical), plugins, DateTimeOffset.UtcNow.ToUnixTimeSeconds()); string status; SentinelPolicy sentinelPolicy = TryLoadPolicy(inputs, token, out status); SentinelDraftExporter.TryWrite(inputs.ConfigRoot, snapshot); Publish(generation, token, snapshot, sentinelPolicy, status, CaptureIntegrityStamps(dictionary, inputs, sentinelPolicy != null)); } catch (OperationCanceledException) { } catch (Exception ex2) { PublishFailure(generation, token, ex2.GetType().Name); } } private void Publish(long generation, CancellationToken token, AttestationSnapshot snapshot, SentinelPolicy policy, string policyStatus, IntegrityFileStamp[] integrityFiles) { lock (_gate) { if (_disposed || token.IsCancellationRequested || generation != _generation) { return; } if (policy != null) { if (policy.Sequence < _highestPolicySequence || (policy.Sequence == _highestPolicySequence && _highestPolicySequence > 0 && !string.Equals(policy.PayloadDigest, _highestPolicyDigest, StringComparison.Ordinal))) { policyStatus = "PolicyRollbackOrEquivocation"; policy = null; } else if (policy.Sequence > _highestPolicySequence) { _highestPolicySequence = policy.Sequence; _highestPolicyDigest = policy.PayloadDigest; } } _snapshot = snapshot; _policy = policy; _integrityFiles = integrityFiles ?? Array.Empty(); _integrityCompromised = false; _integrityReason = ((policy == null) ? "passport-unavailable" : "verified"); if (policy == null) { _networkProfile = null; } else { AdmissionDecision admissionDecision = AdmissionPolicy.Evaluate(policy, snapshot, "player"); _networkProfile = new SentinelNetworkProfile(snapshot.Digest, snapshot.CapturedUnixSeconds, policy.PayloadDigest, policy.Sequence, policy.Profile, admissionDecision.Disposition); } _status = ((policy == null) ? ("ReadyLocalSnapshot:MonitorOnly:" + policyStatus) : "ReadyLocalSnapshot:VerifiedRsaPolicy"); Evidence.SetPolicySequence(policy?.Sequence ?? 0); } } private void PublishFailure(long generation, CancellationToken token, string failure) { lock (_gate) { if (!_disposed && !token.IsCancellationRequested && generation == _generation) { _snapshot = null; _policy = null; _networkProfile = null; _status = "FailedLocalSnapshot:" + failure; _integrityFiles = Array.Empty(); _integrityCompromised = true; _integrityReason = "snapshot-failed"; Evidence.SetPolicySequence(0L); } } } private static SentinelPolicy TryLoadPolicy(WorkerInputs inputs, CancellationToken token, out string status) { status = "PolicyFilesUnavailable"; if (!TryReadStableBounded(inputs.PolicyPath, 1L, 1048576L, token, out var bytes)) { status = "PolicySizeOrChanged"; return null; } if (!TryReadStableBounded(inputs.SignaturePath, 1L, 1024L, token, out var bytes2)) { status = "SignatureSizeOrChanged"; return null; } if (!TryReadStableBounded(inputs.PublicKeyPath, 1L, 1024L, token, out var bytes3)) { status = "PublicKeySizeOrChanged"; return null; } if (!PinnedRsaPublicKey.TryParse(bytes3, inputs.PinnedPublicKeySha256, out var key, out status)) { return null; } if (!SentinelPolicy.TryDecodeSignatureFile(bytes2, out var signature, out status)) { return null; } if (!SentinelPolicy.TryParseAndVerify(bytes, signature, key, out var policy, out status)) { return null; } long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (policy.IssuedUnixSeconds > num + 300) { status = "PolicyNotYetValid"; return null; } if (policy.ExpiresUnixSeconds != 0L && num >= policy.ExpiresUnixSeconds) { status = "PolicyExpired"; return null; } status = "Verified"; return policy; } private static string HashStablePlugin(string path, long expectedLength, byte[] buffer, CancellationToken token) { if (expectedLength <= 0 || expectedLength > 536870912 || buffer == null || buffer.Length == 0) { throw new InvalidDataException("Plugin file bound failed."); } using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, buffer.Length, FileOptions.SequentialScan); using SHA256 sHA = SHA256.Create(); if (fileStream.Length != expectedLength) { throw new IOException("Plugin changed before local snapshot hashing."); } long num = expectedLength; while (num > 0) { token.ThrowIfCancellationRequested(); int count = (int)Math.Min(buffer.Length, num); int num2 = fileStream.Read(buffer, 0, count); if (num2 <= 0) { throw new EndOfStreamException("Plugin ended during local snapshot hashing."); } sHA.TransformBlock(buffer, 0, num2, buffer, 0); num -= num2; } if (fileStream.ReadByte() != -1 || fileStream.Length != expectedLength) { throw new IOException("Plugin grew during local snapshot hashing."); } sHA.TransformFinalBlock(Array.Empty(), 0, 0); return AttestationPolicy.Hex(sHA.Hash); } private static bool TryReadStableBounded(string path, long minimumBytes, long maximumBytes, CancellationToken token, out byte[] bytes) { bytes = null; try { token.ThrowIfCancellationRequested(); FileInfo fileInfo = new FileInfo(path); if (!fileInfo.Exists || fileInfo.Length < minimumBytes || fileInfo.Length > maximumBytes || fileInfo.Length > int.MaxValue) { return false; } long length = fileInfo.Length; DateTime lastWriteTimeUtc = fileInfo.LastWriteTimeUtc; bytes = new byte[(int)length]; using (FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, Math.Max(1, Math.Min(65536, bytes.Length)), FileOptions.SequentialScan)) { if (fileStream.Length != length) { bytes = null; return false; } int num; for (int i = 0; i < bytes.Length; i += num) { token.ThrowIfCancellationRequested(); num = fileStream.Read(bytes, i, bytes.Length - i); if (num <= 0) { bytes = null; return false; } } if (fileStream.ReadByte() != -1 || fileStream.Length != length) { bytes = null; return false; } } fileInfo.Refresh(); if (!fileInfo.Exists || fileInfo.Length != length || fileInfo.LastWriteTimeUtc != lastWriteTimeUtc) { bytes = null; return false; } return true; } catch (OperationCanceledException) { throw; } catch { bytes = null; return false; } } private static PluginDescriptor[] CaptureDescriptors() { List list = new List(); if (Chainloader.PluginInfos.Count > 512) { throw new InvalidDataException("Plugin cap exceeded."); } foreach (KeyValuePair item in Chainloader.PluginInfos.OrderBy, string>((KeyValuePair keyValuePair) => keyValuePair.Key, StringComparer.Ordinal)) { if (list.Count >= 512) { throw new InvalidDataException("Plugin cap exceeded."); } PluginInfo value = item.Value; string location = value.Location; if (string.IsNullOrWhiteSpace(location)) { throw new InvalidDataException("Plugin path unavailable."); } string text = value.Metadata.Version.ToString(); if (!SentinelPolicy.CanonicalPluginId(item.Key) || !SentinelPolicy.CanonicalAtom(text, 1, 64)) { throw new InvalidDataException("Plugin identity evidence is invalid."); } string[] array = value.Dependencies.Select((BepInDependency dependency) => dependency.DependencyGUID).Take(65).OrderBy((string id) => id, StringComparer.Ordinal) .ToArray(); if (array.Length > 64) { throw new InvalidDataException("Plugin dependency cap exceeded."); } list.Add(new PluginDescriptor(item.Key, text, Path.GetFullPath(location), array, Array.Empty())); } return list.ToArray(); } private static WorkerInputs CaptureInputs(string configDirectory) { string fullPath = Path.GetFullPath(configDirectory ?? string.Empty); return new WorkerInputs(fullPath, Resolve(fullPath, SentinelConfig.PolicyFile?.Value), Resolve(fullPath, SentinelConfig.SignatureFile?.Value), Resolve(fullPath, SentinelConfig.PublicKeyFile?.Value), SentinelConfig.TrustedPublicKeySha256?.Value ?? string.Empty); } private static IntegrityFileStamp[] CaptureIntegrityStamps(IReadOnlyDictionary pluginFiles, WorkerInputs inputs, bool includePolicyAssets) { List result = new List(pluginFiles.Count + 3); foreach (KeyValuePair item in pluginFiles.OrderBy, string>((KeyValuePair value) => value.Key, PathComparer)) { result.Add(new IntegrityFileStamp(item.Key, item.Value.Length, item.Value.LastWriteUtcTicks, policyAsset: false)); } if (includePolicyAssets) { AddPolicy(inputs.PolicyPath); AddPolicy(inputs.SignaturePath); AddPolicy(inputs.PublicKeyPath); } return result.ToArray(); void AddPolicy(string path) { FileInfo fileInfo = new FileInfo(path); if (!fileInfo.Exists) { throw new IOException("Verified passport asset disappeared."); } result.Add(new IntegrityFileStamp(fileInfo.FullName, fileInfo.Length, fileInfo.LastWriteTimeUtc.Ticks, policyAsset: true)); } } private static string Resolve(string root, string configured) { if (string.IsNullOrWhiteSpace(configured)) { return string.Empty; } if (!Path.IsPathRooted(configured)) { return Path.GetFullPath(Path.Combine(root, configured)); } return Path.GetFullPath(configured); } private static void CancelAndDispose(CancellationTokenSource cancellation) { if (cancellation == null) { return; } try { cancellation.Cancel(); } catch { } try { cancellation.Dispose(); } catch { } } } [HarmonyPatch(typeof(ZNet), "LoadWorld")] internal static class SentinelTransitionBackup { private static readonly object Gate = new object(); private static SentinelRuntime _runtime; private static ManualLogSource _log; private static string _stateRoot; internal static void Attach(SentinelRuntime runtime, ManualLogSource log, string configRoot) { lock (Gate) { _runtime = runtime; _log = log; _stateRoot = Path.Combine(Path.GetFullPath(configRoot), "RunicSentinel", "transitions"); } } internal static void Detach() { lock (Gate) { _runtime = null; _log = null; _stateRoot = null; } } internal static string CreateVerifiedBackupNow(string reason) { SentinelRuntime runtime; string stateRoot; lock (Gate) { runtime = _runtime; stateRoot = _stateRoot; } ZNet instance = ZNet.instance; World world = ZNet.World; if (runtime == null || (Object)(object)instance == (Object)null || !instance.IsServer() || world == null) { return "no-world-loaded"; } IMigrationBackupService backups = SafetyIntegrationApi.Backups; if (backups == null) { throw new InvalidOperationException("runic-safety-backup-unavailable"); } List list = new List(); List list2 = new List(); string text = Path.Combine(stateRoot, "staging", Guid.NewGuid().ToString("N")); try { AddBackupSource(world, world.GetDBPath(), "world-database", "world.db", text, list, list2); AddBackupSource(world, world.GetMetaPath(), "world-metadata", "world.fwl", text, list, list2); if (list.Count == 0) { throw new FileNotFoundException("world-backup-sources-missing"); } MigrationBackupRequest val = backups.CreateDefaultRequest(string.IsNullOrWhiteSpace(reason) ? "runic-sentinel-admin-backup" : reason, (IEnumerable)list); MigrationBackupResult val2 = backups.CreateBackup(val, CancellationToken.None); string empty = string.Empty; if (val2 == null || !val2.Succeeded || !backups.ValidateBackup(val2.BackupDirectory, ref empty)) { throw new IOException("verified-world-backup-failed-" + (((val2 != null) ? val2.FailureCode : null) ?? empty ?? "unknown")); } return val2.CorrelationId; } finally { foreach (string item in list2) { try { if (File.Exists(item)) { File.Delete(item); } } catch { } } try { if (Directory.Exists(text) && Directory.GetFileSystemEntries(text).Length == 0) { Directory.Delete(text, recursive: false); } } catch { } } } [HarmonyPrefix] private static void BeforeWorldLoad(ZNet __instance) { ConfigEntry backupBeforeTransitions = SentinelConfig.BackupBeforeTransitions; if ((backupBeforeTransitions != null && !backupBeforeTransitions.Value) || (Object)(object)__instance == (Object)null || !__instance.IsServer()) { return; } SentinelRuntime runtime; ManualLogSource log; string stateRoot; lock (Gate) { runtime = _runtime; log = _log; stateRoot = _stateRoot; } World world = ZNet.World; if (runtime == null || world == null) { return; } if (!runtime.TryGetTransitionFingerprint(out var fingerprint)) { if (SentinelConfig.RemoteAdmissionMode != SentinelRemoteAdmissionMode.Required) { return; } runtime.RecordAdmissionFailure("sentinel-transition-profile-unverified"); throw new InvalidOperationException("Raven's Gate cannot verify the active profile before world load."); } string path = Path.Combine(stateRoot, __instance.GetWorldUID() + ".state"); if (string.Equals(ReadMarker(path), fingerprint, StringComparison.Ordinal)) { return; } IMigrationBackupService backups = SafetyIntegrationApi.Backups; if (backups == null) { runtime.RecordAdmissionFailure("sentinel-transition-backup-unavailable"); throw new InvalidOperationException("Runic Safety backup service is required before this profile transition."); } List sources = new List(); List staged = new List(); string staging = Path.Combine(stateRoot, "staging", Guid.NewGuid().ToString("N")); try { Add(world.GetDBPath(), "world-database", "world.db"); Add(world.GetMetaPath(), "world-metadata", "world.fwl"); if (sources.Count == 0) { throw new FileNotFoundException("No existing world files were available for the required transition backup."); } MigrationBackupRequest val = backups.CreateDefaultRequest("runic-sentinel-profile-transition", (IEnumerable)sources); MigrationBackupResult val2 = backups.CreateBackup(val, CancellationToken.None); string empty = string.Empty; if (val2 == null || !val2.Succeeded || !backups.ValidateBackup(val2.BackupDirectory, ref empty)) { runtime.RecordAdmissionFailure("sentinel-transition-backup-failed"); throw new IOException("Required profile-transition backup failed: " + (((val2 != null) ? val2.FailureCode : null) ?? empty ?? "unknown")); } WriteMarker(path, fingerprint); if (log != null) { log.LogWarning((object)("Raven's Gate created and verified a world backup before applying a new modpack or policy profile. Correlation: " + val2.CorrelationId + ".")); } } finally { foreach (string item in staged) { try { if (File.Exists(item)) { File.Delete(item); } } catch { } } try { if (Directory.Exists(staging) && Directory.GetFileSystemEntries(staging).Length == 0) { Directory.Delete(staging, recursive: false); } } catch { } } void Add(string text, string logicalName, string stagingName) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Invalid comparison between Unknown and I4 //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Expected O, but got Unknown //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Expected O, but got Unknown if (!string.IsNullOrEmpty(text)) { if ((int)world.m_fileSource == 2) { if (FileHelpers.Exists(text, (FileSource)2)) { Directory.CreateDirectory(staging); string text2 = Path.Combine(staging, stagingName); FileHelpers.FileCopyOutFromCloud(text, text2, true); if (!File.Exists(text2)) { throw new IOException("Steam Cloud world staging did not produce a readable file."); } staged.Add(text2); sources.Add(new MigrationBackupSource(text2, logicalName)); } } else if (File.Exists(text)) { sources.Add(new MigrationBackupSource(text, logicalName)); } } } } private static string ReadMarker(string path) { try { if (!File.Exists(path)) { return string.Empty; } string text = File.ReadAllText(path, Encoding.ASCII).Trim(); return SentinelPolicy.IsLowerHex(text, 64) ? text : string.Empty; } catch { return string.Empty; } } private static void AddBackupSource(World world, string path, string logicalName, string stagingName, string staging, ICollection sources, ICollection staged) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Invalid comparison between Unknown and I4 //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Expected O, but got Unknown //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Expected O, but got Unknown if (string.IsNullOrEmpty(path)) { return; } if ((int)world.m_fileSource == 2) { if (FileHelpers.Exists(path, (FileSource)2)) { Directory.CreateDirectory(staging); string text = Path.Combine(staging, stagingName); FileHelpers.FileCopyOutFromCloud(path, text, true); if (!File.Exists(text)) { throw new IOException("cloud-world-staging-failed"); } staged.Add(text); sources.Add(new MigrationBackupSource(text, logicalName)); } } else if (File.Exists(path)) { sources.Add(new MigrationBackupSource(path, logicalName)); } } private static void WriteMarker(string path, string value) { Directory.CreateDirectory(Path.GetDirectoryName(path)); string text = path + ".tmp"; byte[] bytes = Encoding.ASCII.GetBytes(value + "\n"); using (FileStream fileStream = new FileStream(text, FileMode.Create, FileAccess.Write, FileShare.None, 4096, 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); } } } internal static class SentinelTransportIdentity { private static readonly FieldInfo SteamConnectionField = typeof(ZSteamSocket).GetField("m_con", BindingFlags.Instance | BindingFlags.NonPublic); internal static bool TryResolvePeer(ZNetPeer peer, out string authority, out string subject) { authority = string.Empty; subject = string.Empty; if (peer == null || !peer.IsReady()) { return false; } ISocket socket = peer.m_socket; ZSteamSocket val = (ZSteamSocket)(object)((socket is ZSteamSocket) ? socket : null); if (val != null && TrySteam(val, out subject)) { authority = "steam"; return true; } ISocket socket2 = peer.m_socket; ZPlayFabSocket val2 = (ZPlayFabSocket)(object)((socket2 is ZPlayFabSocket) ? socket2 : null); if (val2 != null && TryPlayFab(val2, out subject)) { authority = "playfab.entity"; return true; } return false; } internal static bool TryResolveLocal(out string authority, out string subject) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) authority = string.Empty; subject = string.Empty; try { ulong steamID = SteamUser.GetSteamID().m_SteamID; if (steamID == 0L) { return false; } authority = "steam"; subject = steamID.ToString(CultureInfo.InvariantCulture); return true; } catch { return false; } } private static bool TrySteam(ZSteamSocket steam, out string subject) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_006b: 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_0081: Unknown result type (might be due to invalid IL or missing references) subject = string.Empty; if (steam == null || SteamConnectionField == null) { return false; } try { string hostName = steam.GetHostName(); string value = ((object)steam.GetPeerID()/*cast due to .constrained prefix*/).ToString(); if (!(SteamConnectionField.GetValue(steam) is HSteamNetConnection handle) || !TryConnectionInfo(handle, out var info)) { return false; } string value2 = ((object)((SteamNetworkingIdentity)(ref info.m_identityRemote)).GetSteamID()/*cast due to .constrained prefix*/).ToString(); if ((info.m_nFlags & 1) != 0 || !Canonical(hostName, out var parsed) || !Canonical(value, out var parsed2) || !Canonical(value2, out var parsed3) || parsed != parsed2 || parsed2 != parsed3) { return false; } subject = parsed2.ToString(CultureInfo.InvariantCulture); return true; } catch { return false; } } private static bool TryConnectionInfo(HSteamNetConnection handle, out SteamNetConnectionInfo_t info) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) info = default(SteamNetConnectionInfo_t); try { return ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsDedicated()) ? SteamGameServerNetworkingSockets.GetConnectionInfo(handle, ref info) : SteamNetworkingSockets.GetConnectionInfo(handle, ref info); } catch { info = default(SteamNetConnectionInfo_t); return false; } } private static bool TryPlayFab(ZPlayFabSocket socket, out string subject) { subject = string.Empty; try { string text = typeof(ZPlayFabSocket).GetField("m_remotePlayerId", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(socket) as string; if (string.IsNullOrWhiteSpace(text) || text.Length > 256 || text.Trim() != text) { return false; } subject = text; return true; } catch { return false; } } private static bool Canonical(string value, out ulong parsed) { if (ulong.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out parsed) && parsed != 0L) { return parsed.ToString(CultureInfo.InvariantCulture) == value; } return false; } } } namespace RunicSentinel.Core { internal static class AttestationPolicy { internal const int MaximumPlugins = 512; internal const int MaximumRelations = 64; internal static bool TryCanonicalize(IEnumerable source, out IReadOnlyList plugins, out string canonical, out string failure) { plugins = Array.Empty(); canonical = string.Empty; failure = string.Empty; if (source == null) { failure = "MissingAttestation"; return false; } List list = new List(); foreach (AttestedPlugin item in source) { if (list.Count >= 512) { failure = "PluginCap"; return false; } list.Add(item); } AttestedPlugin[] array = list.ToArray(); Array.Sort(array, (AttestedPlugin a, AttestedPlugin b) => string.CompareOrdinal(a?.Id, b?.Id)); HashSet hashSet = new HashSet(StringComparer.Ordinal); StringBuilder stringBuilder = new StringBuilder(Math.Min(65536, 128 + array.Length * 96)); stringBuilder.Append("RUNIC-ATTESTATION/1\n"); AttestedPlugin[] array2 = array; foreach (AttestedPlugin attestedPlugin in array2) { if (attestedPlugin == null || !SentinelPolicy.CanonicalPluginId(attestedPlugin.Id) || !SentinelPolicy.CanonicalAtom(attestedPlugin.Version, 1, 64) || !SentinelPolicy.IsLowerHex(attestedPlugin.Sha256, 64) || !hashSet.Add(attestedPlugin.Id) || attestedPlugin.Dependencies.Count > 64 || attestedPlugin.Capabilities.Count > 64) { failure = "PluginEvidence"; return false; } bool valid; string[] value = CanonicalRelations(attestedPlugin.Dependencies, out valid); bool valid2; string[] value2 = CanonicalRelations(attestedPlugin.Capabilities, out valid2); if (!valid || !valid2) { failure = "PluginRelations"; return false; } stringBuilder.Append(attestedPlugin.Id).Append('|').Append(attestedPlugin.Version) .Append('|') .Append(attestedPlugin.Sha256) .Append('|') .Append(string.Join(",", value)) .Append('|') .Append(string.Join(",", value2)) .Append('\n'); if (stringBuilder.Length > 1048576) { failure = "AttestationSize"; return false; } } plugins = array; canonical = stringBuilder.ToString(); return true; } internal static string Digest(string canonical) { using SHA256 sHA = SHA256.Create(); return Hex(sHA.ComputeHash(Encoding.UTF8.GetBytes(canonical ?? string.Empty))); } internal static bool TryComputeNonceBinding(string nonceHex, string snapshotDigest, out string response) { response = string.Empty; if (!SentinelPolicy.IsLowerHex(nonceHex, 64) || !SentinelPolicy.IsLowerHex(snapshotDigest, 64)) { return false; } using SHA256 sHA = SHA256.Create(); response = Hex(sHA.ComputeHash(Encoding.ASCII.GetBytes("RUNIC-NONCE-BINDING/2\n" + nonceHex + "\n" + snapshotDigest))); return true; } internal static string Hex(byte[] bytes) { StringBuilder stringBuilder = new StringBuilder(bytes.Length * 2); foreach (byte b in bytes) { stringBuilder.Append(b.ToString("x2")); } return stringBuilder.ToString(); } private static string[] CanonicalRelations(IReadOnlyList values, out bool valid) { valid = true; string[] array = values.ToArray(); Array.Sort(array, (IComparer?)StringComparer.Ordinal); string text = null; string[] array2 = array; foreach (string text2 in array2) { if (!SentinelPolicy.CanonicalAtom(text2, 1, 128) || text2 == text) { valid = false; return Array.Empty(); } text = text2; } return array; } } internal static class AdmissionPolicy { internal static AdmissionDecision Evaluate(SentinelPolicy policy, AttestationSnapshot snapshot, string role) { if (policy == null || snapshot == null) { return new AdmissionDecision(AdmissionDisposition.Unavailable, 0L, string.Empty, new AdmissionFinding[1] { new AdmissionFinding("PolicyUnavailable", string.Empty, "No verified server policy is active.", FindingConfidence.High) }); } if (!AttestationPolicy.TryCanonicalize(snapshot.Plugins, out var plugins, out var canonical, out var failure) || !string.Equals(AttestationPolicy.Digest(canonical), snapshot.Digest, StringComparison.Ordinal)) { return new AdmissionDecision(AdmissionDisposition.Deny, policy.Sequence, policy.Profile, new AdmissionFinding[1] { new AdmissionFinding("InvalidAttestation", string.Empty, (failure.Length == 0) ? "DigestMismatch" : failure, FindingConfidence.Conclusive) }); } List findings = new List(); Dictionary dictionary = plugins.ToDictionary((AttestedPlugin attestedPlugin) => attestedPlugin.Id, StringComparer.Ordinal); AdmissionDisposition disposition = AdmissionDisposition.Allow; foreach (SentinelPolicyRule rule in policy.Rules) { AttestedPlugin value; bool flag = dictionary.TryGetValue(rule.Id, out value); if (rule.Classification == PluginClassification.Required && !flag) { Add("RequiredMissing", rule.Id, FindingConfidence.Conclusive, AdmissionDisposition.Deny); } else { if (!flag) { continue; } if (rule.Classification == PluginClassification.Forbidden) { Add("ForbiddenPresent", rule.Id, FindingConfidence.Conclusive, AdmissionDisposition.Deny); continue; } if (rule.Classification == PluginClassification.ServerOnly && !string.Equals(role, "server", StringComparison.Ordinal)) { Add("ServerOnlyOnClient", rule.Id, FindingConfidence.Conclusive, AdmissionDisposition.Deny); continue; } if (rule.Classification == PluginClassification.AdministratorOnly && !string.Equals(role, "administrator", StringComparison.Ordinal)) { Add("AdministratorOnly", rule.Id, FindingConfidence.Conclusive, AdmissionDisposition.Deny); continue; } bool num = rule.Version == "*" || rule.Version == value.Version; bool flag2 = rule.Sha256 == "*" || rule.Sha256 == value.Sha256; if (!num || !flag2) { AdmissionDisposition next = ((rule.Classification == PluginClassification.Required) ? AdmissionDisposition.Deny : AdmissionDisposition.Quarantine); Add("PluginMismatch", rule.Id, FindingConfidence.VeryHigh, next); } if (rule.Classification == PluginClassification.Quarantined) { Add("PolicyQuarantine", rule.Id, FindingConfidence.High, AdmissionDisposition.Quarantine); } else if (rule.Classification == PluginClassification.Unmanaged) { Add("GrayListPresent", rule.Id, FindingConfidence.Informational, AdmissionDisposition.Allow); } } } HashSet hashSet = new HashSet(policy.Rules.Select((SentinelPolicyRule sentinelPolicyRule) => sentinelPolicyRule.Id), StringComparer.Ordinal); foreach (AttestedPlugin item in plugins) { if (!hashSet.Contains(item.Id)) { if (policy.Unknown == PluginClassification.Forbidden) { Add("UnknownForbidden", item.Id, FindingConfidence.High, AdmissionDisposition.Deny); } else if (policy.Unknown == PluginClassification.Quarantined) { Add("UnknownQuarantine", item.Id, FindingConfidence.Moderate, AdmissionDisposition.Quarantine); } } } return new AdmissionDecision(disposition, policy.Sequence, policy.Profile, findings); void Add(string rule, string id, FindingConfidence confidence, AdmissionDisposition admissionDisposition) { findings.Add(new AdmissionFinding(rule, id, rule, confidence)); if (admissionDisposition == AdmissionDisposition.Deny || disposition == AdmissionDisposition.Allow) { disposition = admissionDisposition; } } } } internal sealed class EvidenceLedger : ISentinelEvidenceService { private sealed class ProviderState { internal string ModuleId { get; } internal Queue Entries { get; } internal long Token { get; set; } internal long Accepted { get; set; } internal long Dropped { get; set; } internal ProviderState(string moduleId) { ModuleId = moduleId; Entries = new Queue(8); } } private sealed class ProviderSink : ISentinelEvidenceSink { private readonly EvidenceLedger _owner; private readonly string _moduleId; private readonly long _token; internal ProviderSink(EvidenceLedger owner, string moduleId, long token) { _owner = owner; _moduleId = moduleId; _token = token; } public bool TryAppend(string actor, string rule, string correlationId, FindingConfidence confidence, EnforcementAction requestedAction, string detail, out SecurityEvidence accepted) { return _owner.TryAppend(_moduleId, _token, actor, rule, correlationId, confidence, requestedAction, detail, out accepted); } } private sealed class ProviderLease : ISentinelEvidenceProviderLease, IDisposable { private EvidenceLedger _owner; private readonly long _token; public string ProviderModuleId { get; } public bool IsActive { get { if (_owner != null) { return _owner.IsLeaseActive(ProviderModuleId, _token); } return false; } } public ISentinelEvidenceSink Sink { get; } internal ProviderLease(EvidenceLedger owner, string moduleId, long token, ISentinelEvidenceSink sink) { _owner = owner; ProviderModuleId = moduleId; _token = token; Sink = sink; } public void Dispose() { EvidenceLedger owner = _owner; _owner = null; owner?.Release(ProviderModuleId, _token); } } internal const int Capacity = 256; internal const int MaximumProviders = 32; internal const int CapacityPerProvider = 8; private readonly object _gate = new object(); private readonly Dictionary _providers = new Dictionary(StringComparer.Ordinal); private long _sequence; private long _token; private long _policySequence; internal event Action Accepted; public ISentinelEvidenceProviderLease RegisterProvider(string providerId) { string text = RunicIdentifier.Require(providerId, "providerId"); lock (_gate) { if (_token == long.MaxValue) { throw new InvalidOperationException("The evidence provider token space is exhausted."); } _providers.TryGetValue(text, out var value); if (value != null && IsActiveLocked(value)) { throw new InvalidOperationException("An active lease owns this evidence provider ID."); } if (value == null) { PruneInactiveLocked(); if (_providers.Count >= 32) { throw new InvalidOperationException("The bounded evidence provider registry is full."); } value = new ProviderState(text); _providers.Add(text, value); } long num = ++_token; long token = (value.Token = num); ProviderSink sink = new ProviderSink(this, text, token); return new ProviderLease(this, text, token, sink); } } public EvidenceReadSnapshot ReadAfter(long sequence, int maximum) { maximum = Math.Max(0, Math.Min(256, maximum)); long num = Math.Max(0L, sequence); lock (_gate) { List list = new List(); List list2 = new List(_providers.Count); foreach (ProviderState value in _providers.Values) { foreach (SecurityEvidence entry in value.Entries) { if (entry.Sequence > num) { list.Add(entry); } } list2.Add(new EvidenceProviderStatus(value.ModuleId, IsActiveLocked(value), value.Entries.Count, value.Accepted, value.Dropped)); } list.Sort((SecurityEvidence left, SecurityEvidence right) => left.Sequence.CompareTo(right.Sequence)); if (list.Count > maximum) { list.RemoveRange(maximum, list.Count - maximum); } list2.Sort((EvidenceProviderStatus left, EvidenceProviderStatus right) => StringComparer.Ordinal.Compare(left.ProviderModuleId, right.ProviderModuleId)); return new EvidenceReadSnapshot(_sequence, _policySequence, list, list2); } } internal void SetPolicySequence(long sequence) { lock (_gate) { _policySequence = Math.Max(0L, sequence); } } private bool TryAppend(string moduleId, long token, string actor, string rule, string correlationId, FindingConfidence confidence, EnforcementAction requestedAction, string detail, out SecurityEvidence accepted) { accepted = null; Action action; lock (_gate) { if (!_providers.TryGetValue(moduleId, out var value) || value.Token != token) { return false; } if (!IsActiveLocked(value) || !BoundedSafe(actor, 128) || !BoundedSafe(rule, 128) || !BoundedSafe(correlationId, 128) || !BoundedSafe(detail, 512) || !IsConfidence(confidence) || !IsAction(requestedAction) || _sequence == long.MaxValue) { value.Dropped = SaturatingIncrement(value.Dropped); return false; } EnforcementAction enforcementAction = requestedAction; if (confidence <= FindingConfidence.Moderate && enforcementAction > EnforcementAction.Warn) { enforcementAction = EnforcementAction.Warn; } if (confidence < FindingConfidence.Conclusive && enforcementAction == EnforcementAction.Ban) { enforcementAction = EnforcementAction.Quarantine; } long sequence = _sequence + 1; accepted = new SecurityEvidence(sequence, DateTimeOffset.UtcNow.ToUnixTimeSeconds(), moduleId, actor, rule, correlationId, confidence, requestedAction, enforcementAction, _policySequence, detail); _sequence = sequence; if (value.Entries.Count == 8) { value.Entries.Dequeue(); value.Dropped = SaturatingIncrement(value.Dropped); } value.Entries.Enqueue(accepted); value.Accepted = SaturatingIncrement(value.Accepted); action = this.Accepted; } try { action?.Invoke(accepted); } catch { } return true; } private static bool IsActiveLocked(ProviderState state) { return state.Token > 0; } private void Release(string moduleId, long token) { lock (_gate) { if (_providers.TryGetValue(moduleId, out var value) && value.Token == token) { value.Token = 0L; } } } private bool IsLeaseActive(string moduleId, long token) { lock (_gate) { ProviderState value; return _providers.TryGetValue(moduleId, out value) && value.Token == token && IsActiveLocked(value); } } private void PruneInactiveLocked() { List list = new List(); foreach (KeyValuePair provider in _providers) { if (!IsActiveLocked(provider.Value)) { list.Add(provider.Key); } } for (int i = 0; i < list.Count; i++) { _providers.Remove(list[i]); } } private static bool BoundedSafe(string value, int maximum) { if (value == null || value.Length == 0 || value.Length > maximum) { return false; } for (int i = 0; i < value.Length; i++) { UnicodeCategory unicodeCategory = char.GetUnicodeCategory(value[i]); if (char.IsControl(value[i]) || unicodeCategory == UnicodeCategory.Format || unicodeCategory == UnicodeCategory.Surrogate) { return false; } } return true; } private static bool IsConfidence(FindingConfidence value) { if (value >= FindingConfidence.Informational) { return value <= FindingConfidence.Conclusive; } return false; } private static bool IsAction(EnforcementAction value) { if (value >= EnforcementAction.Log) { return value <= EnforcementAction.Ban; } return false; } private static long SaturatingIncrement(long value) { if (value != long.MaxValue) { return value + 1; } return value; } } internal sealed class PinnedRsaPublicKey { internal const int ModulusBytes = 384; internal const int MaximumFileBytes = 1024; private static readonly byte[] CanonicalExponent = new byte[3] { 1, 0, 1 }; private readonly byte[] _modulus; internal string Fingerprint { get; } private PinnedRsaPublicKey(byte[] modulus, string fingerprint) { _modulus = (byte[])modulus.Clone(); Fingerprint = fingerprint; } internal RSAParameters CreateParameters() { return new RSAParameters { Modulus = (byte[])_modulus.Clone(), Exponent = (byte[])CanonicalExponent.Clone() }; } internal static bool TryParse(byte[] payload, string pinnedFingerprint, out PinnedRsaPublicKey key, out string failure) { key = null; failure = string.Empty; if (payload == null || payload.Length == 0 || payload.Length > 1024) { failure = "PublicKeySize"; return false; } if (!SentinelPolicy.IsLowerHex(pinnedFingerprint, 64)) { failure = "PublicKeyPin"; return false; } string text; try { text = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true).GetString(payload); } catch { failure = "PublicKeyUtf8"; return false; } if (text.IndexOf('\r') >= 0 || text.IndexOf('\0') >= 0 || !text.EndsWith("\n", StringComparison.Ordinal)) { failure = "PublicKeyCanonical"; return false; } string[] array = text.Split('\n'); if (array.Length != 4 || array[0] != "RUNIC-RSA-PUBLIC/1" || !array[1].StartsWith("modulus=", StringComparison.Ordinal) || array[2] != "exponent=AQAB" || array[3].Length != 0) { failure = "PublicKeyCanonical"; return false; } byte[] array2; try { array2 = Convert.FromBase64String(array[1].Substring(8)); } catch { failure = "PublicKeyEncoding"; return false; } if (array2.Length != 384 || (array2[0] & 0x80) == 0 || Convert.ToBase64String(array2) != array[1].Substring(8)) { failure = "PublicKeyShape"; return false; } string text2; using (SHA256 sHA = SHA256.Create()) { text2 = SentinelPolicy.Hex(sHA.ComputeHash(payload)); } if (!SentinelPolicy.FixedTimeHexEquals(text2, pinnedFingerprint)) { failure = "PublicKeyPinMismatch"; return false; } key = new PinnedRsaPublicKey(array2, text2); return true; } } internal static class RunicIdentifier { internal static bool IsValid(string value) { if (string.IsNullOrEmpty(value) || value.Length > 128) { return false; } bool flag = false; bool flag2 = false; foreach (char c in value) { if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { flag = true; flag2 = false; continue; } switch (c) { case '-': if (!flag || flag2) { return false; } flag2 = true; break; case '.': if (!flag || flag2) { return false; } flag = false; flag2 = false; break; default: return false; } } if (flag) { return !flag2; } return false; } internal static string Require(string value, string parameterName) { if (!IsValid(value)) { throw new ArgumentException("Runic identifiers must be lowercase dot-separated ASCII tokens.", parameterName); } return value; } } internal enum SentinelRemoteAdmissionMode { Disabled, Optional, Required } internal interface ISentinelNetworkProfileSource { bool TryGetNetworkProfile(out SentinelNetworkProfile profile); } internal sealed class SentinelNetworkProfile { internal string SnapshotDigest { get; } internal long SnapshotCapturedUnixSeconds { get; } internal string PolicyDigest { get; } internal long PolicySequence { get; } internal string PolicyProfile { get; } internal AdmissionDisposition Disposition { get; } internal SentinelNetworkProfile(string snapshotDigest, long snapshotCapturedUnixSeconds, string policyDigest, long policySequence, string policyProfile, AdmissionDisposition disposition) { SnapshotDigest = SecurityContractValidation.RequireLowerHex(snapshotDigest, 64, "snapshotDigest"); PolicyDigest = SecurityContractValidation.RequireLowerHex(policyDigest, 64, "policyDigest"); if (snapshotCapturedUnixSeconds < 0) { throw new ArgumentOutOfRangeException("snapshotCapturedUnixSeconds"); } if (policySequence < 0) { throw new ArgumentOutOfRangeException("policySequence"); } if (!SecurityContractValidation.IsDisposition(disposition)) { throw new ArgumentOutOfRangeException("disposition"); } SnapshotCapturedUnixSeconds = snapshotCapturedUnixSeconds; PolicySequence = policySequence; PolicyProfile = SecurityContractValidation.RequireAtom(policyProfile, 1, 64, "policyProfile"); Disposition = disposition; } } internal sealed class SentinelAdmissionEnvelope { internal string RequestId { get; set; } internal string Version { get; set; } internal string SnapshotDigest { get; set; } internal long SnapshotCapturedUnixSeconds { get; set; } internal string PolicyDigest { get; set; } internal long PolicySequence { get; set; } internal string PolicyProfile { get; set; } internal AdmissionDisposition Disposition { get; set; } internal long IssuedUnixSeconds { get; set; } } internal readonly struct SentinelAdmissionCheck { internal bool Compatible { get; } internal string Reason { get; } internal SentinelAdmissionCheck(bool compatible, string reason) { Compatible = compatible; Reason = reason ?? string.Empty; } } internal sealed class SentinelNetworkCompatibility : IDisposable { private sealed class PendingRequest { internal SentinelAdmissionEnvelope Envelope { get; } internal long DeadlineTicks { get; } internal long NextAttemptTicks { get; set; } internal int Attempts { get; set; } internal PendingRequest(SentinelAdmissionEnvelope envelope, long deadlineTicks, long nextAttemptTicks) { Envelope = envelope; DeadlineTicks = deadlineTicks; NextAttemptTicks = nextAttemptTicks; } } private sealed class CachedDecision { internal string Key { get; } internal bool Compatible { get; } internal string Reason { get; } internal long ExpiresTicks { get; } internal CachedDecision(string key, bool compatible, string reason, long expiresTicks) { Key = key; Compatible = compatible; Reason = reason; ExpiresTicks = expiresTicks; } } internal const string RequestRpcName = "runic.sentinel.admission.request.v1"; internal const string ResponseRpcName = "runic.sentinel.admission.response.v1"; internal const string PluginVersion = "1.0.0"; internal const int WireSchema = 1; internal const int MaximumRequestBytes = 1024; internal const int MaximumResponseBytes = 256; internal const int MaximumCachedRequests = 256; private const int TerminalMarker = 1397642289; private const long MaximumRequestAgeSeconds = 300L; private const long MaximumFutureSkewSeconds = 60L; private const long CacheLifetimeTicks = 600000000L; private const long RequestTimeoutTicks = 60000000L; private const long RetryIntervalTicks = 20000000L; private const long SuccessIntervalTicks = 300000000L; private const long FailureIntervalTicks = 100000000L; private const int MaximumAttempts = 3; private const string EvidenceProviderId = "runic.sentinel.network"; private readonly object _gate = new object(); private readonly ISentinelNetworkProfileSource _profiles; private readonly SentinelRemoteAdmissionMode _mode; private readonly ISentinelEvidenceProviderLease _evidence; private readonly Dictionary _decisions = new Dictionary(StringComparer.Ordinal); private readonly Queue _decisionOrder = new Queue(); private ZRoutedRpc _registeredRpc; private PendingRequest _pending; private long _nextRequestTicks; private bool _disposed; internal bool IsActive { get { lock (_gate) { return !_disposed && _mode != SentinelRemoteAdmissionMode.Disabled && _registeredRpc != null; } } } internal SentinelNetworkCompatibility(ISentinelNetworkProfileSource profiles, EvidenceLedger evidence, SentinelRemoteAdmissionMode mode) { _profiles = profiles ?? throw new ArgumentNullException("profiles"); if (mode < SentinelRemoteAdmissionMode.Disabled || mode > SentinelRemoteAdmissionMode.Required) { throw new ArgumentOutOfRangeException("mode"); } _mode = mode; if (mode != SentinelRemoteAdmissionMode.Disabled) { _evidence = (evidence ?? throw new ArgumentNullException("evidence")).RegisterProvider("runic.sentinel.network"); } } internal void Tick() { lock (_gate) { if (_disposed || _mode == SentinelRemoteAdmissionMode.Disabled) { return; } ZRoutedRpc instance = ZRoutedRpc.instance; ZNet instance2 = ZNet.instance; if (instance == null || (Object)(object)instance2 == (Object)null) { return; } RegisterLocked(instance); long ticks = DateTime.UtcNow.Ticks; ExpireDecisionsLocked(ticks); if (instance2.IsServer()) { _pending = null; return; } ZNetPeer serverPeer = instance2.GetServerPeer(); long num = serverPeer?.m_uid ?? 0; if (serverPeer == null || serverPeer.m_uid != num || !serverPeer.IsReady()) { _pending = null; _nextRequestTicks = Math.Max(_nextRequestTicks, ticks + 100000000); } else if (_pending != null) { if (ticks >= _pending.DeadlineTicks) { _pending = null; _nextRequestTicks = ticks + 100000000; } else if (ticks >= _pending.NextAttemptTicks && _pending.Attempts < 3) { SendLocked(instance, num, _pending, ticks); } } else { if (ticks < _nextRequestTicks) { return; } SentinelNetworkProfile profile; try { if (!_profiles.TryGetNetworkProfile(out profile) || profile == null) { _nextRequestTicks = ticks + 100000000; return; } } catch (Exception) { _nextRequestTicks = ticks + 100000000; return; } SentinelAdmissionEnvelope envelope = new SentinelAdmissionEnvelope { RequestId = Guid.NewGuid().ToString("N"), Version = "1.0.0", SnapshotDigest = profile.SnapshotDigest, SnapshotCapturedUnixSeconds = profile.SnapshotCapturedUnixSeconds, PolicyDigest = profile.PolicyDigest, PolicySequence = profile.PolicySequence, PolicyProfile = profile.PolicyProfile, Disposition = profile.Disposition, IssuedUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds() }; _pending = new PendingRequest(envelope, ticks + 60000000, ticks); SendLocked(instance, num, _pending, ticks); } } } public void Dispose() { lock (_gate) { if (_disposed) { return; } _disposed = true; _pending = null; _registeredRpc = null; _decisions.Clear(); _decisionOrder.Clear(); } try { _evidence?.Dispose(); } catch (Exception) { } } internal static SentinelAdmissionCheck Evaluate(SentinelNetworkProfile local, SentinelAdmissionEnvelope remote, long nowUnixSeconds) { if (local == null) { return Fail("sentinel-server-policy-unavailable"); } if (remote == null || !CanonicalRequestId(remote.RequestId) || !string.Equals(remote.Version, "1.0.0", StringComparison.Ordinal) || !LowerHex(remote.SnapshotDigest) || !LowerHex(remote.PolicyDigest) || !CanonicalProfile(remote.PolicyProfile) || remote.PolicySequence < 0 || remote.SnapshotCapturedUnixSeconds < 0 || remote.Disposition < AdmissionDisposition.Unavailable || remote.Disposition > AdmissionDisposition.Deny) { return Fail("sentinel-request-malformed"); } if (remote.IssuedUnixSeconds < nowUnixSeconds - 300 || remote.IssuedUnixSeconds > nowUnixSeconds + 60 || remote.SnapshotCapturedUnixSeconds > nowUnixSeconds + 60) { return Fail("sentinel-request-stale"); } if (!string.Equals(local.SnapshotDigest, remote.SnapshotDigest, StringComparison.Ordinal)) { return Fail("sentinel-snapshot-mismatch"); } if (!string.Equals(local.PolicyDigest, remote.PolicyDigest, StringComparison.Ordinal) || local.PolicySequence != remote.PolicySequence || !string.Equals(local.PolicyProfile, remote.PolicyProfile, StringComparison.Ordinal)) { return Fail("sentinel-policy-mismatch"); } if (local.Disposition != AdmissionDisposition.Allow || remote.Disposition != AdmissionDisposition.Allow) { return Fail("sentinel-policy-not-allow"); } return new SentinelAdmissionCheck(compatible: true, "compatible"); } internal static ZPackage WriteRequest(SentinelAdmissionEnvelope value) { //IL_000e: 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_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Expected O, but got Unknown if (value == null) { throw new ArgumentNullException("value"); } ZPackage val = new ZPackage(); val.Write(1); val.Write(value.RequestId ?? string.Empty); val.Write(value.Version ?? string.Empty); val.Write(value.SnapshotDigest ?? string.Empty); val.Write(value.SnapshotCapturedUnixSeconds); val.Write(value.PolicyDigest ?? string.Empty); val.Write(value.PolicySequence); val.Write(value.PolicyProfile ?? string.Empty); val.Write((int)value.Disposition); val.Write(value.IssuedUnixSeconds); val.Write(1397642289); return val; } internal static bool TryReadRequest(ZPackage package, out SentinelAdmissionEnvelope value) { value = null; try { if (package == null || package.Size() <= 0 || package.Size() > 1024 || package.ReadInt() != 1) { return false; } SentinelAdmissionEnvelope sentinelAdmissionEnvelope = new SentinelAdmissionEnvelope { RequestId = package.ReadString(), Version = package.ReadString(), SnapshotDigest = package.ReadString(), SnapshotCapturedUnixSeconds = package.ReadLong(), PolicyDigest = package.ReadString(), PolicySequence = package.ReadLong(), PolicyProfile = package.ReadString(), Disposition = (AdmissionDisposition)package.ReadInt(), IssuedUnixSeconds = package.ReadLong() }; if (package.ReadInt() != 1397642289 || package.GetPos() != package.Size() || !CanonicalRequestId(sentinelAdmissionEnvelope.RequestId) || sentinelAdmissionEnvelope.Version == null || sentinelAdmissionEnvelope.Version.Length < 1 || sentinelAdmissionEnvelope.Version.Length > 32 || !LowerHex(sentinelAdmissionEnvelope.SnapshotDigest) || !LowerHex(sentinelAdmissionEnvelope.PolicyDigest) || !CanonicalProfile(sentinelAdmissionEnvelope.PolicyProfile)) { return false; } value = sentinelAdmissionEnvelope; return true; } catch (Exception) { value = null; return false; } } private void RegisterLocked(ZRoutedRpc routed) { if (_registeredRpc != routed) { routed.Register("runic.sentinel.admission.request.v1", (Action)ReceiveRequest); routed.Register("runic.sentinel.admission.response.v1", (Action)ReceiveResponse); _registeredRpc = routed; _pending = null; _nextRequestTicks = 0L; } } private void ReceiveRequest(long sender, ZPackage package) { lock (_gate) { if (_disposed || _mode == SentinelRemoteAdmissionMode.Disabled) { return; } ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; if ((Object)(object)instance == (Object)null || instance2 == null || !instance.IsServer()) { return; } ZNetPeer peer = instance.GetPeer(sender); if (peer == null || peer.m_uid != sender || !peer.IsReady()) { return; } long ticks = DateTime.UtcNow.Ticks; long nowUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); ExpireDecisionsLocked(ticks); if (!TryReadRequest(package, out var value)) { RecordLocked(sender, string.Empty, "sentinel-request-malformed", authenticated: false); DisconnectRequiredLocked(instance, peer); return; } string key = sender + ":" + value.RequestId; if (_decisions.TryGetValue(key, out var value2) && value2.ExpiresTicks > ticks) { SendResponseLocked(instance2, sender, value.RequestId, value2.Compatible, value2.Reason); if (!value2.Compatible) { DisconnectRequiredLocked(instance, peer); } return; } SentinelNetworkProfile profile = null; try { _profiles.TryGetNetworkProfile(out profile); } catch (Exception) { profile = null; } SentinelAdmissionCheck check = Evaluate(profile, value, nowUnixSeconds); CacheLocked(key, check, ticks + 600000000); SendResponseLocked(instance2, sender, value.RequestId, check.Compatible, check.Reason); if (!check.Compatible) { RecordLocked(sender, value.RequestId, check.Reason, authenticated: true); DisconnectRequiredLocked(instance, peer); } } } private void ReceiveResponse(long sender, ZPackage package) { lock (_gate) { if (_disposed || _pending == null) { return; } ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; if ((Object)(object)instance == (Object)null || instance2 == null || instance.IsServer()) { return; } long num = instance.GetServerPeer()?.m_uid ?? 0; ZNetPeer peer = instance.GetPeer(sender); if (sender == num && peer != null && peer.m_uid == sender && peer.IsReady() && TryReadResponse(package, out var requestId, out var compatible, out var reason) && string.Equals(requestId, _pending.Envelope.RequestId, StringComparison.Ordinal)) { _pending = null; _nextRequestTicks = DateTime.UtcNow.Ticks + (compatible ? 300000000 : 100000000); if (!compatible) { RecordLocked(sender, requestId, reason, authenticated: true); } } } } private static void SendLocked(ZRoutedRpc routed, long serverId, PendingRequest pending, long nowTicks) { ZPackage val = WriteRequest(pending.Envelope); if (val.Size() <= 1024) { routed.InvokeRoutedRPC(serverId, "runic.sentinel.admission.request.v1", new object[1] { val }); pending.Attempts++; pending.NextAttemptTicks = nowTicks + 20000000; } } private static void SendResponseLocked(ZRoutedRpc routed, long peerId, string requestId, bool compatible, string reason) { ZPackage val = WriteResponse(requestId, compatible, reason); if (val.Size() <= 256) { routed.InvokeRoutedRPC(peerId, "runic.sentinel.admission.response.v1", new object[1] { val }); } } private static ZPackage WriteResponse(string requestId, bool compatible, string reason) { //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_003b: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(1); val.Write(requestId ?? string.Empty); val.Write(compatible); val.Write(BoundedReason(reason)); val.Write(1397642289); return val; } private static bool TryReadResponse(ZPackage package, out string requestId, out bool compatible, out string reason) { requestId = string.Empty; compatible = false; reason = string.Empty; try { if (package == null || package.Size() <= 0 || package.Size() > 256 || package.ReadInt() != 1) { return false; } requestId = package.ReadString(); compatible = package.ReadBool(); reason = package.ReadString(); return CanonicalRequestId(requestId) && CanonicalReason(reason) && package.ReadInt() == 1397642289 && package.GetPos() == package.Size(); } catch (Exception) { requestId = string.Empty; compatible = false; reason = string.Empty; return false; } } private void RecordLocked(long peerId, string requestId, string reason, bool authenticated) { ISentinelEvidenceSink sentinelEvidenceSink = _evidence?.Sink; if (sentinelEvidenceSink != null) { string correlationId = (CanonicalRequestId(requestId) ? requestId : ("malformed-" + peerId)); string detail = "reason-" + BoundedReason(reason) + (authenticated ? ".authenticated-peer" : ".peer-bound"); sentinelEvidenceSink.TryAppend("peer:" + peerId, "sentinel.remote-admission", correlationId, (_mode == SentinelRemoteAdmissionMode.Required) ? FindingConfidence.High : FindingConfidence.Moderate, (_mode == SentinelRemoteAdmissionMode.Required) ? EnforcementAction.Disconnect : EnforcementAction.Warn, detail, out var _); } } private void DisconnectRequiredLocked(ZNet network, ZNetPeer exactPeer) { if (_mode == SentinelRemoteAdmissionMode.Required && exactPeer != null) { network.Disconnect(exactPeer); } } private void CacheLocked(string key, SentinelAdmissionCheck check, long expiresTicks) { while (_decisionOrder.Count >= 256) { CachedDecision cachedDecision = _decisionOrder.Dequeue(); if (_decisions.TryGetValue(cachedDecision.Key, out var value) && cachedDecision == value) { _decisions.Remove(cachedDecision.Key); } } CachedDecision cachedDecision2 = new CachedDecision(key, check.Compatible, check.Reason, expiresTicks); _decisions[key] = cachedDecision2; _decisionOrder.Enqueue(cachedDecision2); } private void ExpireDecisionsLocked(long nowTicks) { while (_decisionOrder.Count > 0 && _decisionOrder.Peek().ExpiresTicks <= nowTicks) { CachedDecision cachedDecision = _decisionOrder.Dequeue(); if (_decisions.TryGetValue(cachedDecision.Key, out var value) && cachedDecision == value) { _decisions.Remove(cachedDecision.Key); } } } private static SentinelAdmissionCheck Fail(string reason) { return new SentinelAdmissionCheck(compatible: false, reason); } private static bool CanonicalRequestId(string value) { if (value == null || value.Length != 32) { return false; } foreach (char c in value) { if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) { return false; } } return true; } private static bool LowerHex(string value) { if (value == null || value.Length != 64) { return false; } foreach (char c in value) { if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) { return false; } } return true; } private static bool CanonicalProfile(string value) { if (value == null || value.Length < 1 || value.Length > 64) { return false; } foreach (char c in value) { if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '.' && c != '-' && c != '_') { return false; } } return true; } private static bool CanonicalReason(string value) { if (value == null || value.Length < 1 || value.Length > 96) { return false; } foreach (char c in value) { if ((c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '-' && c != '.') { return false; } } return true; } private static string BoundedReason(string value) { if (!CanonicalReason(value)) { return "sentinel-request-rejected"; } return value; } } internal sealed class SentinelPolicyRule { internal PluginClassification Classification { get; } internal string Id { get; } internal string Version { get; } internal string Sha256 { get; } internal SentinelPolicyRule(PluginClassification classification, string id, string version, string sha256) { Classification = classification; Id = id; Version = version; Sha256 = sha256; } } internal enum SentinelModuleScope { Both, Client, Server } internal sealed class SentinelModuleRule { internal SentinelModuleScope Scope { get; } internal string Id { get; } internal string Version { get; } internal int Protocol { get; } internal IReadOnlyList Capabilities { get; } internal SentinelModuleRule(SentinelModuleScope scope, string id, string version, int protocol, IReadOnlyList capabilities) { Scope = scope; Id = id; Version = version; Protocol = protocol; Capabilities = capabilities; } } internal sealed class SentinelAdministratorRole { internal string Authority { get; } internal string Subject { get; } internal string CanonicalKey => Authority + ":" + Uri.EscapeDataString(Subject); internal SentinelAdministratorRole(string authority, string subject) { Authority = authority; Subject = subject; } } internal sealed class SentinelPolicy { internal const int MaximumBytes = 1048576; internal const int MaximumRules = 2048; internal const int SignatureBytes = 384; internal const int MaximumSignatureFileBytes = 1024; internal int FormatVersion { get; } internal string Profile { get; } internal long Sequence { get; } internal long IssuedUnixSeconds { get; } internal long ExpiresUnixSeconds { get; } internal PluginClassification Unknown { get; } internal IReadOnlyList Rules { get; } internal IReadOnlyList Modules { get; } internal IReadOnlyList Administrators { get; } internal IReadOnlyList BannedUsers { get; } internal string PayloadDigest { get; } internal SentinelPolicy(int formatVersion, string profile, long sequence, long issuedUnixSeconds, long expiresUnixSeconds, PluginClassification unknown, IReadOnlyList rules, IReadOnlyList modules, IReadOnlyList administrators, IReadOnlyList bannedUsers, string payloadDigest) { FormatVersion = formatVersion; Profile = profile; Sequence = sequence; IssuedUnixSeconds = issuedUnixSeconds; ExpiresUnixSeconds = expiresUnixSeconds; Unknown = unknown; Rules = rules; Modules = modules; Administrators = administrators; BannedUsers = bannedUsers; PayloadDigest = payloadDigest; } internal static bool TryDecodeSignatureFile(byte[] file, out byte[] signature, out string failure) { signature = null; failure = string.Empty; if (file == null || file.Length == 0 || file.Length > 1024) { failure = "SignatureSize"; return false; } string text; try { text = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true).GetString(file); } catch { failure = "SignatureUtf8"; return false; } if (text.IndexOf('\r') >= 0 || text.IndexOf('\0') >= 0 || !text.EndsWith("\n", StringComparison.Ordinal) || text.IndexOf('\n') != text.Length - 1) { failure = "SignatureCanonical"; return false; } string text2 = text.Substring(0, text.Length - 1); try { signature = Convert.FromBase64String(text2); } catch { failure = "SignatureEncoding"; return false; } if (signature.Length != 384 || Convert.ToBase64String(signature) != text2) { signature = null; failure = "SignatureShape"; return false; } return true; } internal static bool TryParseAndVerify(byte[] payload, byte[] signature, PinnedRsaPublicKey publicKey, out SentinelPolicy policy, out string failure) { policy = null; failure = string.Empty; if (payload == null || payload.Length == 0 || payload.Length > 1048576) { failure = "PolicySize"; return false; } if (signature == null || signature.Length != 384 || publicKey == null) { failure = "SignatureShape"; return false; } try { using RSA rSA = RSA.Create(); rSA.ImportParameters(publicKey.CreateParameters()); if (rSA.KeySize != 3072 || !rSA.VerifyData(payload, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)) { failure = "SignatureMismatch"; return false; } } catch { failure = "SignatureVerification"; return false; } string text; try { text = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true).GetString(payload); } catch { failure = "PolicyUtf8"; return false; } if (text.IndexOf('\r') >= 0 || text.IndexOf('\0') >= 0 || !text.EndsWith("\n", StringComparison.Ordinal)) { failure = "PolicyCanonical"; return false; } string[] array = text.Split('\n'); bool num = array.Length >= 7 && array[0] == "RUNIC-SENTINEL/2"; bool flag = array.Length >= 8 && array[0] == "RUNIC-SENTINEL/3"; if ((!num && !flag) || array[^1].Length != 0) { failure = "PolicyHeader"; return false; } if (!TryRequired(array[1], "profile=", 64, out var value) || !TryCanonicalLong(array[2], "sequence=", allowZero: false, out var value2) || !TryCanonicalLong(array[3], "issued=", allowZero: true, out var value3) || !TryCanonicalLong(array[4], "expires=", allowZero: true, out var value4) || (value4 != 0L && value4 <= value3) || !TryUnknown(array[5], out var value5)) { failure = "PolicyPreamble"; return false; } int num2 = 6; if (flag) { if (array[6] != "unknown-capability=Forbidden") { failure = "CapabilityDefault"; return false; } num2 = 7; } List list = new List(); List list2 = new List(); List list3 = new List(); List list4 = new List(); string text2 = null; string text3 = null; string text4 = null; string text5 = null; int num3 = 0; for (int i = num2; i < array.Length - 1; i++) { string text6 = array[i]; if (text6.StartsWith("rule=", StringComparison.Ordinal) && num3 <= 0) { if (list.Count >= 2048) { failure = "PolicyRule"; return false; } string[] array2 = text6.Substring(5).Split('|'); if (array2.Length != 4 || !TryClassification(array2[0], out var value6) || !CanonicalPluginId(array2[1]) || !CanonicalVersionOrWildcard(array2[2]) || !CanonicalHashOrWildcard(array2[3])) { failure = "PolicyRule"; return false; } if (text2 != null && string.CompareOrdinal(text2, array2[1]) >= 0) { failure = "RuleOrder"; return false; } text2 = array2[1]; list.Add(new SentinelPolicyRule(value6, array2[1], array2[2], array2[3])); continue; } if (flag && text6.StartsWith("module=", StringComparison.Ordinal) && num3 <= 1) { num3 = 1; if (list2.Count >= 128) { failure = "ModuleCap"; return false; } string[] array3 = text6.Substring(7).Split('|'); if (array3.Length != 5 || !TryModuleScope(array3[0], out var scope) || !CanonicalPluginId(array3[1]) || !CanonicalVersionOrWildcard(array3[2]) || !TryCanonicalInt(array3[3], out var value7) || !TryCapabilities(array3[4], out var capabilities)) { failure = "ModuleRule"; return false; } if (text3 != null && string.CompareOrdinal(text3, array3[1]) >= 0) { failure = "ModuleOrder"; return false; } text3 = array3[1]; list2.Add(new SentinelModuleRule(scope, array3[1], array3[2], value7, capabilities)); continue; } if (flag && text6.StartsWith("role=", StringComparison.Ordinal) && num3 <= 2) { num3 = 2; if (list3.Count >= 256) { failure = "RoleCap"; return false; } string[] array4 = text6.Substring(5).Split('|'); if (array4.Length != 2 || !CanonicalAuthority(array4[0]) || !TryCanonicalSubject(array4[1], out var subject)) { failure = "RoleRule"; return false; } string text7 = array4[0] + ":" + array4[1]; if (text4 != null && string.CompareOrdinal(text4, text7) >= 0) { failure = "RoleOrder"; return false; } text4 = text7; list3.Add(new SentinelAdministratorRole(array4[0], subject)); continue; } if (flag && text6.StartsWith("ban=", StringComparison.Ordinal) && num3 <= 3) { num3 = 3; if (list4.Count >= 4096) { failure = "BanCap"; return false; } string[] array5 = text6.Substring(4).Split('|'); if (array5.Length != 2 || !CanonicalAuthority(array5[0]) || !TryCanonicalSubject(array5[1], out var subject2)) { failure = "BanRule"; return false; } string text8 = array5[0] + ":" + array5[1]; if (text5 != null && string.CompareOrdinal(text5, text8) >= 0) { failure = "BanOrder"; return false; } text5 = text8; list4.Add(new SentinelAdministratorRole(array5[0], subject2)); continue; } failure = (flag ? "PolicyEntry" : "PolicyRule"); return false; } string payloadDigest; using (SHA256 sHA = SHA256.Create()) { payloadDigest = Hex(sHA.ComputeHash(payload)); } policy = new SentinelPolicy(flag ? 3 : 2, value, value2, value3, value4, value5, list.AsReadOnly(), list2.AsReadOnly(), list3.AsReadOnly(), list4.AsReadOnly(), payloadDigest); return true; } internal static bool CanonicalPluginId(string value) { return CanonicalAtom(value, 1, 128); } internal static bool CanonicalVersionOrWildcard(string value) { if (!(value == "*")) { return CanonicalAtom(value, 1, 64); } return true; } internal static bool CanonicalHashOrWildcard(string value) { if (!(value == "*")) { return IsLowerHex(value, 64); } return true; } internal static bool CanonicalAtom(string value, int minimum, int maximum) { if (value == null || value.Length < minimum || value.Length > maximum) { return false; } foreach (char c in value) { if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '.' && c != '-' && c != '_') { return false; } } return true; } internal static bool IsLowerHex(string value, int length) { if (value == null || value.Length != length) { return false; } for (int i = 0; i < value.Length; i++) { if ((value[i] < '0' || value[i] > '9') && (value[i] < 'a' || value[i] > 'f')) { return false; } } return true; } internal static string Hex(byte[] bytes) { StringBuilder stringBuilder = new StringBuilder(bytes.Length * 2); for (int i = 0; i < bytes.Length; i++) { stringBuilder.Append(bytes[i].ToString("x2", CultureInfo.InvariantCulture)); } return stringBuilder.ToString(); } internal static bool FixedTimeHexEquals(string left, string right) { if (left == null || right == null) { return false; } int num = left.Length ^ right.Length; int num2 = Math.Max(left.Length, right.Length); for (int i = 0; i < num2; i++) { char c = ((i < left.Length) ? left[i] : '\0'); char c2 = ((i < right.Length) ? right[i] : '\0'); num |= c ^ c2; } return num == 0; } private static bool TryRequired(string line, string prefix, int maximum, out string value) { value = string.Empty; if (!line.StartsWith(prefix, StringComparison.Ordinal)) { return false; } value = line.Substring(prefix.Length); return CanonicalAtom(value, 1, maximum); } private static bool TryCanonicalLong(string line, string prefix, bool allowZero, out long value) { value = 0L; if (!line.StartsWith(prefix, StringComparison.Ordinal)) { return false; } string text = line.Substring(prefix.Length); if (text.Length == 0 || text.Length > 19 || (text.Length > 1 && text[0] == '0') || !long.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out value)) { return false; } if (!allowZero) { return value > 0; } return value >= 0; } private static bool TryUnknown(string line, out PluginClassification value) { value = PluginClassification.Unknown; if (!line.StartsWith("unknown=", StringComparison.Ordinal)) { return false; } switch (line.Substring(8)) { case "Quarantined": value = PluginClassification.Quarantined; break; case "Unmanaged": value = PluginClassification.Unmanaged; break; case "Forbidden": value = PluginClassification.Forbidden; break; default: return false; } return true; } private static bool TryClassification(string text, out PluginClassification value) { value = PluginClassification.Unknown; switch (text) { case "Required": value = PluginClassification.Required; return true; case "ApprovedOptional": value = PluginClassification.ApprovedOptional; return true; case "ServerOnly": value = PluginClassification.ServerOnly; return true; case "Forbidden": value = PluginClassification.Forbidden; return true; case "Unmanaged": value = PluginClassification.Unmanaged; return true; case "AdministratorOnly": value = PluginClassification.AdministratorOnly; return true; case "Quarantined": value = PluginClassification.Quarantined; return true; default: return false; } } private static bool TryModuleScope(string text, out SentinelModuleScope scope) { scope = SentinelModuleScope.Both; switch (text) { case "Both": return true; case "Client": scope = SentinelModuleScope.Client; return true; case "Server": scope = SentinelModuleScope.Server; return true; default: return false; } } private static bool TryCanonicalInt(string text, out int value) { value = 0; if (text != null && text.Length > 0 && text.Length <= 10 && (text.Length == 1 || text[0] != '0') && int.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out value)) { return value > 0; } return false; } private static bool TryCapabilities(string text, out IReadOnlyList capabilities) { capabilities = Array.Empty(); if (string.IsNullOrEmpty(text)) { return true; } string[] array = text.Split(','); if (array.Length > 64) { return false; } string text2 = null; for (int i = 0; i < array.Length; i++) { if (!CanonicalAtom(array[i], 1, 128) || (text2 != null && string.CompareOrdinal(text2, array[i]) >= 0)) { return false; } text2 = array[i]; } capabilities = Array.AsReadOnly(array); return true; } private static bool CanonicalAuthority(string value) { if (!CanonicalAtom(value, 1, 64)) { return false; } for (int i = 0; i < value.Length; i++) { if (value[i] >= 'A' && value[i] <= 'Z') { return false; } } return true; } private static bool TryCanonicalSubject(string encoded, out string subject) { subject = string.Empty; if (string.IsNullOrEmpty(encoded) || encoded.Length > 768) { return false; } try { subject = Uri.UnescapeDataString(encoded); if (string.IsNullOrEmpty(subject) || subject.Length > 256 || Uri.EscapeDataString(subject) != encoded) { return false; } for (int i = 0; i < subject.Length; i++) { if (char.IsControl(subject[i])) { return false; } } return true; } catch { subject = string.Empty; return false; } } } internal static class SentinelVersion { internal const string Current = "1.2.0"; } } namespace RunicSentinel.Contracts { public enum PluginClassification { Unknown, Required, ApprovedOptional, ServerOnly, Forbidden, Unmanaged, AdministratorOnly, Quarantined } public enum AdmissionDisposition { Unavailable, Allow, Quarantine, Deny } public enum FindingConfidence { Informational, Low, Moderate, High, VeryHigh, Conclusive } public enum EnforcementAction { Log, Validate, Cancel, Warn, Disconnect, Quarantine, Ban } public sealed class AttestedPlugin { public const int MaximumRelations = 64; public const int MaximumInspectedRelations = 256; public string Id { get; } public string Version { get; } public string Sha256 { get; } public IReadOnlyList Dependencies { get; } public IReadOnlyList Capabilities { get; } public AttestedPlugin(string id, string version, string sha256, IEnumerable dependencies, IEnumerable capabilities) { Id = SecurityContractValidation.RequireAtom(id, 1, 128, "id"); Version = SecurityContractValidation.RequireAtom(version, 1, 64, "version"); Sha256 = SecurityContractValidation.RequireLowerHex(sha256, 64, "sha256"); Dependencies = SecurityContractValidation.CopyAtoms(dependencies, 64, 256, "dependencies"); Capabilities = SecurityContractValidation.CopyAtoms(capabilities, 64, 256, "capabilities"); } } public sealed class AttestationSnapshot { public const int MaximumPlugins = 512; public string Digest { get; } public IReadOnlyList Plugins { get; } public long CapturedUnixSeconds { get; } public AttestationSnapshot(string digest, IEnumerable plugins, long capturedUnixSeconds) { Digest = SecurityContractValidation.RequireLowerHex(digest, 64, "digest"); if (capturedUnixSeconds < 0) { throw new ArgumentOutOfRangeException("capturedUnixSeconds"); } List list = new List(); if (plugins != null) { foreach (AttestedPlugin plugin in plugins) { if (list.Count >= 512) { throw new ArgumentOutOfRangeException("plugins"); } list.Add(plugin ?? throw new ArgumentException("Attested plugins cannot be null.", "plugins")); } } Plugins = list.AsReadOnly(); CapturedUnixSeconds = capturedUnixSeconds; } } public sealed class AdmissionFinding { public string Rule { get; } public string PluginId { get; } public string Detail { get; } public FindingConfidence Confidence { get; } public AdmissionFinding(string rule, string pluginId, string detail, FindingConfidence confidence) { Rule = SecurityContractValidation.RequireSafeText(rule, 1, 128, "rule"); PluginId = (string.IsNullOrEmpty(pluginId) ? string.Empty : SecurityContractValidation.RequireAtom(pluginId, 1, 128, "pluginId")); Detail = SecurityContractValidation.RequireSafeText(detail, 1, 512, "detail"); if (!SecurityContractValidation.IsConfidence(confidence)) { throw new ArgumentOutOfRangeException("confidence"); } Confidence = confidence; } } public sealed class AdmissionDecision { public AdmissionDisposition Disposition { get; } public long PolicySequence { get; } public string PolicyProfile { get; } public IReadOnlyList Findings { get; } public AdmissionDecision(AdmissionDisposition disposition, long policySequence, string policyProfile, IEnumerable findings) { if (!SecurityContractValidation.IsDisposition(disposition)) { throw new ArgumentOutOfRangeException("disposition"); } if (policySequence < 0) { throw new ArgumentOutOfRangeException("policySequence"); } Disposition = disposition; PolicySequence = policySequence; PolicyProfile = (string.IsNullOrEmpty(policyProfile) ? string.Empty : SecurityContractValidation.RequireAtom(policyProfile, 1, 64, "policyProfile")); List list = new List(); if (findings != null) { foreach (AdmissionFinding finding in findings) { if (list.Count >= 4096) { throw new ArgumentOutOfRangeException("findings"); } list.Add(finding ?? throw new ArgumentException("Findings cannot contain null.", "findings")); } } Findings = list.AsReadOnly(); } } public sealed class SecurityEvidence { public long Sequence { get; } public long UnixSeconds { get; } public string ProviderModuleId { get; } public string Actor { get; } public string Rule { get; } public string CorrelationId { get; } public FindingConfidence Confidence { get; } public EnforcementAction RequestedAction { get; } public EnforcementAction EffectiveAction { get; } public long PolicySequence { get; } public string Detail { get; } public SecurityEvidence(long sequence, long unixSeconds, string providerModuleId, string actor, string rule, string correlationId, FindingConfidence confidence, EnforcementAction requestedAction, EnforcementAction effectiveAction, long policySequence, string detail) { if (sequence <= 0) { throw new ArgumentOutOfRangeException("sequence"); } if (unixSeconds < 0) { throw new ArgumentOutOfRangeException("unixSeconds"); } ProviderModuleId = RunicIdentifier.Require(providerModuleId, "providerModuleId"); Actor = SecurityContractValidation.RequireSafeText(actor, 1, 128, "actor"); Rule = SecurityContractValidation.RequireSafeText(rule, 1, 128, "rule"); CorrelationId = SecurityContractValidation.RequireSafeText(correlationId, 1, 128, "correlationId"); Detail = SecurityContractValidation.RequireSafeText(detail, 1, 512, "detail"); if (!SecurityContractValidation.IsConfidence(confidence)) { throw new ArgumentOutOfRangeException("confidence"); } if (!SecurityContractValidation.IsAction(requestedAction)) { throw new ArgumentOutOfRangeException("requestedAction"); } if (!SecurityContractValidation.IsAction(effectiveAction)) { throw new ArgumentOutOfRangeException("effectiveAction"); } if (policySequence < 0) { throw new ArgumentOutOfRangeException("policySequence"); } Sequence = sequence; UnixSeconds = unixSeconds; Confidence = confidence; RequestedAction = requestedAction; EffectiveAction = effectiveAction; PolicySequence = policySequence; } } public sealed class EvidenceProviderStatus { public string ProviderModuleId { get; } public bool Active { get; } public int BufferedEntries { get; } public long AcceptedEntries { get; } public long DroppedEntries { get; } public EvidenceProviderStatus(string providerModuleId, bool active, int bufferedEntries, long acceptedEntries, long droppedEntries) { ProviderModuleId = RunicIdentifier.Require(providerModuleId, "providerModuleId"); if (bufferedEntries < 0 || acceptedEntries < 0 || droppedEntries < 0) { throw new ArgumentOutOfRangeException("bufferedEntries"); } Active = active; BufferedEntries = bufferedEntries; AcceptedEntries = acceptedEntries; DroppedEntries = droppedEntries; } } public sealed class EvidenceReadSnapshot { public long NewestSequence { get; } public long PolicySequence { get; } public IReadOnlyList Entries { get; } public IReadOnlyList Providers { get; } public EvidenceReadSnapshot(long newestSequence, long policySequence, IEnumerable entries, IEnumerable providers) { if (newestSequence < 0 || policySequence < 0) { throw new ArgumentOutOfRangeException("newestSequence"); } NewestSequence = newestSequence; PolicySequence = policySequence; Entries = Copy(entries, 256, "entries"); Providers = Copy(providers, 32, "providers"); } private static IReadOnlyList Copy(IEnumerable source, int maximum, string name) where T : class { List list = new List(); if (source != null) { foreach (T item in source) { if (list.Count >= maximum) { throw new ArgumentOutOfRangeException(name); } list.Add(item ?? throw new ArgumentException("Snapshot values cannot be null.", name)); } } return list.AsReadOnly(); } } public interface ISentinelAttestationService { bool ProvidesClientAuthenticityProof { get; } bool TryGetCurrent(out AttestationSnapshot snapshot, out string status); bool TryComputeNonceBinding(string nonceHex, out string bindingHex, out string status); } public interface ISentinelAdmissionService { bool PolicyReady { get; } bool AuthoritativeTransportReady { get; } long PolicySequence { get; } string PolicyProfile { get; } AdmissionDecision Evaluate(AttestationSnapshot snapshot, string role); } public interface ISentinelEvidenceSink { bool TryAppend(string actor, string rule, string correlationId, FindingConfidence confidence, EnforcementAction requestedAction, string detail, out SecurityEvidence accepted); } public interface ISentinelEvidenceProviderLease : IDisposable { string ProviderModuleId { get; } bool IsActive { get; } ISentinelEvidenceSink Sink { get; } } public interface ISentinelEvidenceService { ISentinelEvidenceProviderLease RegisterProvider(string providerId); EvidenceReadSnapshot ReadAfter(long sequence, int maximum); } internal static class SecurityContractValidation { internal static string RequireAtom(string value, int minimum, int maximum, string name) { if (value == null || value.Length < minimum || value.Length > maximum) { throw new ArgumentOutOfRangeException(name); } foreach (char c in value) { if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '.' && c != '-' && c != '_') { throw new ArgumentException("Value is not a canonical atom.", name); } } return value; } internal static string RequireLowerHex(string value, int length, string name) { if (value == null || value.Length != length) { throw new ArgumentOutOfRangeException(name); } foreach (char c in value) { if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) { throw new ArgumentException("Value is not canonical lowercase hexadecimal.", name); } } return value; } internal static string RequireSafeText(string value, int minimum, int maximum, string name) { if (value == null || value.Length < minimum || value.Length > maximum) { throw new ArgumentOutOfRangeException(name); } foreach (char c in value) { UnicodeCategory unicodeCategory = char.GetUnicodeCategory(c); if (char.IsControl(c) || unicodeCategory == UnicodeCategory.Format || unicodeCategory == UnicodeCategory.Surrogate) { throw new ArgumentException("Text contains unsafe control or formatting characters.", name); } } return value; } internal static IReadOnlyList CopyAtoms(IEnumerable values, int maximumUnique, int maximumInspected, string name) { SortedSet sortedSet = new SortedSet(StringComparer.Ordinal); int num = 0; if (values != null) { foreach (string value in values) { if (++num > maximumInspected) { throw new ArgumentOutOfRangeException(name); } string item = RequireAtom(value, 1, 128, name); if (!sortedSet.Add(item)) { throw new ArgumentException("Duplicate relation.", name); } if (sortedSet.Count > maximumUnique) { throw new ArgumentOutOfRangeException(name); } } } return new List(sortedSet).AsReadOnly(); } internal static bool IsClassification(PluginClassification value) { if (value >= PluginClassification.Required) { return value <= PluginClassification.Quarantined; } return false; } internal static bool IsDisposition(AdmissionDisposition value) { if (value >= AdmissionDisposition.Unavailable) { return value <= AdmissionDisposition.Deny; } return false; } internal static bool IsConfidence(FindingConfidence value) { if (value >= FindingConfidence.Informational) { return value <= FindingConfidence.Conclusive; } return false; } internal static bool IsAction(EnforcementAction value) { if (value >= EnforcementAction.Log) { return value <= EnforcementAction.Ban; } return false; } } public static class SentinelCapabilityIds { public const string ProtocolVersion = "3.0"; public const string Admission = "security.admission"; public const string Attestation = "security.attest"; public const string Evidence = "security.evidence"; public const string Roles = "security.roles"; public const string Enforcement = "security.enforcement"; public const string RuntimeIntegrity = "security.runtime-integrity"; public static IReadOnlyList Published { get; } = Array.AsReadOnly(new string[6] { "security.admission", "security.attest", "security.enforcement", "security.evidence", "security.roles", "security.runtime-integrity" }); } } namespace RunicSentinel.Api { public static class SentinelIntegrationApi { private static readonly object Gate = new object(); private static SentinelEnforcementRuntime _service; internal static void Attach(SentinelEnforcementRuntime service) { lock (Gate) { _service = service; } } internal static void Detach(SentinelEnforcementRuntime service) { lock (Gate) { if (_service == service) { _service = null; } } } public static bool ReportRejectedServerRequest(string sourceModuleId, long peerId, string actor, string rule, string correlationId, int confidence, string detail) { if (confidence < 0 || confidence > 5) { return false; } SentinelEnforcementRuntime service; lock (Gate) { service = _service; } return service?.ReportRejectedServerRequest(sourceModuleId, peerId, actor, rule, correlationId, (FindingConfidence)confidence, detail) ?? false; } } }