Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of RunicSentinelServer v1.0.0
RunicSentinelServer.dll
Decompiled 2 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; 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.Admission; 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 Server")] [assembly: AssemblyDescription("Dedicated and listen-host Sentinel authority without client reporting or administrator GUI code")] [assembly: AssemblyCompany("Chazman")] [assembly: AssemblyProduct("Runic Sentinel Server")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: InternalsVisibleTo("RunicSentinelServer.Tests")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyVersion("1.0.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 { [BepInPlugin("chazman.RunicSentinelServer", "Runic Sentinel Server", "1.0.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInIncompatibility("chazman.RunicSentinel")] public sealed class Plugin : BaseUnityPlugin { public const string Guid = "chazman.RunicSentinelServer"; public const string Name = "Runic Sentinel Server"; public const string Version = "1.0.0"; public const string ModuleId = "runic.sentinel.server"; private static Plugin _instance; private SentinelRuntime _runtime; private SentinelEnforcementRuntime _enforcement; private SentinelOperatorCommands _operatorCommands; private SentinelManagedPolicyService _managedPolicy; private SentinelAdminControl _adminControl; private SentinelFlightRecorder _flightRecorder; private Harmony _failClosedHarmony; private Harmony _roleHarmony; private Harmony _authorityHarmony; private ZNet _authorityNetwork; private ZNet _destroyedAuthorityNetwork; private int _refreshRequested; private bool _authorityStarted; private bool _activationFailed; private bool _configuredRoleKnown; private bool _serverRoleConfigured; private bool _unavailableGateLogged; private bool _clientNoticeLogged; private void Awake() { SentinelConfig.Bind(((BaseUnityPlugin)this).Config); ConfigEntry<bool> enabled = SentinelConfig.Enabled; if (enabled != null && !enabled.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Sentinel Server is disabled; no worker or network handlers were created."); return; } _instance = this; try { InstallPermanentFailClosedGate(); } catch (Exception ex) { _activationFailed = true; Shutdown(); ((BaseUnityPlugin)this).Logger.LogError((object)("Runic Sentinel Server could not install its Required-mode safety gate and is inactive: " + ex)); return; } try { InstallRoleObservers(); TryActivateAuthority(ZNet.instance); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Sentinel Server v1.0.0 installed. Authority services remain inert until Valheim selects a dedicated server or listen host role."); } catch (Exception ex2) { _activationFailed = true; ShutdownAuthority(); try { Harmony roleHarmony = _roleHarmony; if (roleHarmony != null) { roleHarmony.UnpatchSelf(); } } catch { } _roleHarmony = null; ((BaseUnityPlugin)this).Logger.LogError((object)("Runic Sentinel Server role bootstrap failed. Required mode remains fail closed through the permanent world-load and connection gates: " + ex2)); } } private void InstallPermanentFailClosedGate() { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Expected O, but got Unknown //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Expected O, but got Unknown MethodInfo methodInfo = RequireInstanceVoid("RPC_ServerHandshake", typeof(ZRpc)); MethodInfo methodInfo2 = RequireInstanceVoid("LoadWorld"); _failClosedHarmony = new Harmony("chazman.RunicSentinelServer.fail-closed"); try { _failClosedHarmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(SentinelServerRoleBootstrap), "BeforeUnavailableServerHandshake", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _failClosedHarmony.Patch((MethodBase)methodInfo2, new HarmonyMethod(typeof(SentinelServerRoleBootstrap), "BeforeUnavailableWorldLoad", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } catch { try { _failClosedHarmony.UnpatchSelf(); } catch { } _failClosedHarmony = null; throw; } } private void InstallRoleObservers() { //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Expected O, but got Unknown //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_011f: Expected O, but got Unknown //IL_0138: Unknown result type (might be due to invalid IL or missing references) //IL_0145: Expected O, but got Unknown //IL_015e: Unknown result type (might be due to invalid IL or missing references) //IL_016b: Expected O, but got Unknown //IL_0184: Unknown result type (might be due to invalid IL or missing references) //IL_0191: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(ZNet), "SetServer", new Type[6] { typeof(bool), typeof(bool), typeof(bool), typeof(string), typeof(string), typeof(World) }, (Type[])null); if (methodInfo == null || !methodInfo.IsStatic || methodInfo.ReturnType != typeof(void)) { throw new MissingMethodException(typeof(ZNet).FullName, "SetServer"); } MethodInfo methodInfo2 = RequireInstanceVoid("Awake"); MethodInfo methodInfo3 = RequireInstanceVoid("OnNewConnection", typeof(ZNetPeer)); MethodInfo methodInfo4 = RequireInstanceVoid("OnDestroy"); _roleHarmony = new Harmony("chazman.RunicSentinelServer.authority-role"); _roleHarmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(SentinelServerRoleBootstrap), "AfterSetServer", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _roleHarmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(typeof(SentinelServerRoleBootstrap), "AfterZNetAwake", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _roleHarmony.Patch((MethodBase)methodInfo3, (HarmonyMethod)null, new HarmonyMethod(typeof(SentinelServerRoleBootstrap), "AfterNewConnection", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _roleHarmony.Patch((MethodBase)methodInfo4, (HarmonyMethod)null, new HarmonyMethod(typeof(SentinelServerRoleBootstrap), "AfterZNetDestroy", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } private static MethodInfo RequireInstanceVoid(string name, params Type[] parameters) { MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(ZNet), name, parameters ?? Type.EmptyTypes, (Type[])null); if (methodInfo == null || methodInfo.IsStatic || methodInfo.ReturnType != typeof(void)) { throw new MissingMethodException(typeof(ZNet).FullName, name); } return methodInfo; } internal static void ObserveConfiguredRole(bool server) { Plugin instance = _instance; if (!((Object)(object)instance == (Object)null)) { instance._configuredRoleKnown = true; instance._serverRoleConfigured = server; instance._destroyedAuthorityNetwork = null; if (server) { instance.TryActivateAuthority(null); return; } instance.ShutdownAuthority(); instance.LogClientInertOnce(); } } internal static void ObserveNetwork(ZNet network) { _instance?.TryActivateAuthority(network); } internal static void ObserveConnection(ZNet network, ZNetPeer peer) { Plugin instance = _instance; if (!((Object)(object)instance == (Object)null)) { instance.TryActivateAuthority(network); if (instance._authorityStarted && (Object)(object)network != (Object)null && network.IsServer()) { instance._authorityNetwork = network; SentinelNetworkCompatibility.ObserveConnection(network, peer); } else { instance.BlockUnavailableConnection(network, peer, null); } } } internal static void ObserveNetworkDestroyed(ZNet network) { Plugin instance = _instance; if ((Object)(object)instance != (Object)null && instance._authorityStarted && network == instance._authorityNetwork) { instance._destroyedAuthorityNetwork = network; instance.ShutdownAuthority(); } } internal static bool AllowUnavailableServerHandshake(ZNet network, ZRpc rpc) { Plugin instance = _instance; if (!((Object)(object)instance == (Object)null)) { return !instance.BlockUnavailableConnection(network, null, rpc); } return true; } internal static void GuardUnavailableWorldLoad(ZNet network) { Plugin instance = _instance; if ((Object)(object)instance == (Object)null || !instance.ShouldBlockUnavailable(network)) { return; } instance.LogUnavailableGateOnce(); throw new InvalidOperationException("Runic Sentinel Server Required authority is unavailable; world load was blocked."); } private void TryActivateAuthority(ZNet network) { //IL_016d: Unknown result type (might be due to invalid IL or missing references) //IL_0177: Expected O, but got Unknown if (_authorityStarted) { if ((Object)(object)network != (Object)null && network.IsServer()) { _authorityNetwork = network; _destroyedAuthorityNetwork = null; } } else { if (_activationFailed || network == _destroyedAuthorityNetwork) { return; } bool flag = _configuredRoleKnown && _serverRoleConfigured; if ((Object)(object)network != (Object)null) { try { flag |= network.IsServer(); } catch { } } if (flag) { try { _runtime = new SentinelRuntime(); _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); _authorityHarmony = new Harmony("chazman.RunicSentinelServer.authority"); _authorityHarmony.PatchAll(typeof(Plugin).Assembly); SentinelConfig.Changed += Refresh; _runtime.Start(Paths.ConfigPath); _authorityNetwork = (((Object)(object)network != (Object)null && network.IsServer()) ? network : null); _authorityStarted = true; ((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Sentinel Server authority services started. Required admission now gates the exact direct connection and the dedicated console/admin backend is available."); return; } catch (Exception ex) { _activationFailed = true; ShutdownAuthority(); ((BaseUnityPlugin)this).Logger.LogError((object)("Runic Sentinel Server authority startup failed closed: " + ex)); return; } } if ((Object)(object)network != (Object)null || _configuredRoleKnown) { LogClientInertOnce(); } } } internal static bool UnavailableAuthorityFailsClosed(SentinelRemoteAdmissionMode mode, bool authoritative, bool authorityStarted) { if (authoritative && !authorityStarted) { return mode == SentinelRemoteAdmissionMode.Required; } return false; } private bool ShouldBlockUnavailable(ZNet network) { bool authoritative = false; try { authoritative = (Object)(object)network != (Object)null && network.IsServer(); } catch { } return UnavailableAuthorityFailsClosed(SentinelConfig.RemoteAdmissionMode, authoritative, _authorityStarted); } private bool BlockUnavailableConnection(ZNet network, ZNetPeer peer, ZRpc rpc) { if (!ShouldBlockUnavailable(network)) { return false; } LogUnavailableGateOnce(); try { ZNetPeer val = peer; if (val == null && rpc != null) { foreach (ZNetPeer peer2 in network.GetPeers()) { if (peer2 != null && peer2.m_rpc == rpc) { val = peer2; break; } } } if (val != null) { network.Disconnect(val); } } catch { } return true; } private void LogUnavailableGateOnce() { if (!_unavailableGateLogged) { _unavailableGateLogged = true; ((BaseUnityPlugin)this).Logger.LogError((object)"Runic Sentinel Server Required authority is unavailable. World loading and native client admission remain blocked until the server is restarted successfully."); } } private void LogClientInertOnce() { if (!_clientNoticeLogged) { _clientNoticeLogged = true; ((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Sentinel Server detected a non-authoritative client and remains fully inert. Install Runic Sentinel Client in player-only profiles."); } } private void Refresh() { Interlocked.Exchange(ref _refreshRequested, 1); } private void Update() { ZNet instance = ZNet.instance; if (_authorityStarted && (Object)(object)instance != (Object)null && !instance.IsServer()) { ShutdownAuthority(); LogClientInertOnce(); return; } if (_authorityStarted && (Object)(object)instance != (Object)null && instance.IsServer()) { _authorityNetwork = instance; _destroyedAuthorityNetwork = null; } if (!_authorityStarted) { if ((Object)(object)instance != (Object)null) { TryActivateAuthority(instance); } return; } try { _runtime?.TickNetwork(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Sentinel server admission tick stopped safely: " + ex.Message)); } try { _runtime?.TickIntegrity(); } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Sentinel server integrity check failed closed: " + ex2.Message)); } try { _adminControl?.Tick(); } catch (Exception ex3) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Sentinel server administrator transport stopped safely: " + ex3.Message)); } try { _operatorCommands?.TickDedicatedConsole(); } catch (Exception ex4) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Sentinel server console input 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 server policy refresh failed closed: " + ex5.Message)); } } private void OnDestroy() { Shutdown(); } private void Shutdown() { if (_instance == this) { _instance = null; } ShutdownAuthority(); try { Harmony roleHarmony = _roleHarmony; if (roleHarmony != null) { roleHarmony.UnpatchSelf(); } } catch { } _roleHarmony = null; try { Harmony failClosedHarmony = _failClosedHarmony; if (failClosedHarmony != null) { failClosedHarmony.UnpatchSelf(); } } catch { } _failClosedHarmony = null; } private void ShutdownAuthority() { SentinelConfig.Changed -= Refresh; Interlocked.Exchange(ref _refreshRequested, 0); _authorityStarted = false; _authorityNetwork = null; try { Harmony authorityHarmony = _authorityHarmony; if (authorityHarmony != null) { authorityHarmony.UnpatchSelf(); } } catch { } _authorityHarmony = 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; } } internal static class SentinelServerRoleBootstrap { internal static void AfterSetServer([HarmonyArgument(0)] bool server) { Plugin.ObserveConfiguredRole(server); } internal static void AfterZNetAwake(ZNet __instance) { Plugin.ObserveNetwork(__instance); } internal static void AfterNewConnection(ZNet __instance, [HarmonyArgument(0)] ZNetPeer peer) { Plugin.ObserveConnection(__instance, peer); } internal static void AfterZNetDestroy(ZNet __instance) { Plugin.ObserveNetworkDestroyed(__instance); } [HarmonyPriority(800)] internal static bool BeforeUnavailableServerHandshake(ZNet __instance, [HarmonyArgument(0)] ZRpc rpc) { return Plugin.AllowUnavailableServerHandshake(__instance, rpc); } [HarmonyPriority(800)] internal static void BeforeUnavailableWorldLoad(ZNet __instance) { Plugin.GuardUnavailableWorldLoad(__instance); } } internal static class SentinelConfig { internal static ConfigEntry<bool> Enabled; internal static ConfigEntry<string> PolicyFile; internal static ConfigEntry<string> SignatureFile; internal static ConfigEntry<string> PublicKeyFile; internal static ConfigEntry<string> TrustedPublicKeySha256; internal static ConfigEntry<string> RemoteAdmissionPolicy; internal static ConfigEntry<int> IntegrityCheckSeconds; internal static ConfigEntry<bool> BackupBeforeTransitions; internal static ConfigEntry<int> VeryHighDisconnectCount; internal static ConfigEntry<int> HighDisconnectCount; internal static ConfigEntry<int> EnforcementWindowSeconds; internal static 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 Enabled = config.Bind<bool>("General", "Enabled", true, "Enable bounded Sentinel local-snapshot, RSA policy, and evidence services. Sampled at startup; false registers nothing and starts no worker."); PolicyFile = config.Bind<string>("Policy", "ManifestFile", "RunicSentinel.policy", "Canonical RUNIC-SENTINEL/3 policy path, relative to BepInEx/config unless absolute."); SignatureFile = config.Bind<string>("Policy", "SignatureFile", "RunicSentinel.policy.sig", "Canonical Base64 detached RSA-3072/SHA-256 PKCS#1 v1.5 signature path."); PublicKeyFile = config.Bind<string>("Policy", "PublicKeyFile", "RunicSentinel.policy.pub", "Canonical RUNIC-RSA-PUBLIC/1 verification public key path. The optional F3 workflow keeps its private key in a separate server-only directory."); TrustedPublicKeySha256 = config.Bind<string>("Policy", "TrustedPublicKeySha256", string.Empty, "Required lowercase SHA-256 of the exact canonical public-key file. Empty or mismatched pins keep Sentinel monitor-only."); RemoteAdmissionPolicy = config.Bind<string>("Remote Admission", "Policy", "Optional", "Sampled at startup; changing it requires a restart. Required withholds native admission and denies a missing, stale, malformed, or signed-policy-incompatible client report after a bounded grace period. Optional records evidence without delaying or disconnecting. Disabled does not register the direct Sentinel admission protocol."); IntegrityCheckSeconds = config.Bind<int>("Runtime Integrity", "CheckIntervalSeconds", 15, "Metadata-check loaded plugin DLLs and active signed-passport files at this interval. A detected runtime change denies new strict admissions until restart. Range 5-300 seconds."); BackupBeforeTransitions = config.Bind<bool>("Transition Safety", "BackupWorldBeforeProfileChange", true, "Before a server loads an existing world with a different signed policy or plugin snapshot, require a verified Runic Safety backup of the world database and metadata."); VeryHighDisconnectCount = config.Bind<int>("Automatic Enforcement", "VeryHighFindingsBeforeDisconnect", 2, new ConfigDescription("Disconnect after this many very-high-confidence violations in the enforcement window.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 10), Array.Empty<object>())); HighDisconnectCount = config.Bind<int>("Automatic Enforcement", "HighFindingsBeforeDisconnect", 3, new ConfigDescription("Disconnect after this many high-confidence violations in the enforcement window.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 20), Array.Empty<object>())); EnforcementWindowSeconds = config.Bind<int>("Automatic Enforcement", "FindingWindowSeconds", 60, new ConfigDescription("Rolling violation window used by graduated automatic enforcement.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(10, 600), Array.Empty<object>())); PolicyFile.SettingChanged += Notify; SignatureFile.SettingChanged += Notify; PublicKeyFile.SettingChanged += Notify; TrustedPublicKeySha256.SettingChanged += Notify; } private static void Notify(object sender, EventArgs args) { SentinelConfig.Changed?.Invoke(); } } } namespace RunicSentinel.Admission { internal sealed class AdmissionPluginEvidence { internal string Id { get; } internal string Version { get; } internal string Sha256 { get; } internal AdmissionPluginEvidence(string id, string version, string sha256) { Id = AdmissionValidation.RequireAtom(id, 1, 128, "id"); Version = AdmissionValidation.RequireAtom(version, 1, 64, "version"); Sha256 = AdmissionValidation.RequireLowerHex(sha256, 64, "sha256"); } } internal sealed class AdmissionClientProfile { internal long CapturedUnixSeconds { get; } internal string Digest { get; } internal IReadOnlyList<AdmissionPluginEvidence> Plugins { get; } internal AdmissionClientProfile(long capturedUnixSeconds, string digest, IList<AdmissionPluginEvidence> plugins) { if (capturedUnixSeconds < 0) { throw new ArgumentOutOfRangeException("capturedUnixSeconds"); } CapturedUnixSeconds = capturedUnixSeconds; Digest = AdmissionValidation.RequireLowerHex(digest, 64, "digest"); if (plugins == null) { throw new ArgumentNullException("plugins"); } Plugins = new ReadOnlyCollection<AdmissionPluginEvidence>(new List<AdmissionPluginEvidence>(plugins)); } } internal sealed class AdmissionChallenge { private readonly byte[] _nonce; internal string RequestId { get; } internal byte[] Nonce => (byte[])_nonce.Clone(); internal long IssuedUnixSeconds { get; } internal long DeadlineUnixSeconds { get; } internal AdmissionChallenge(string requestId, byte[] nonce, long issuedUnixSeconds, long deadlineUnixSeconds) { RequestId = AdmissionValidation.RequireLowerHex(requestId, 32, "requestId"); if (nonce == null || nonce.Length != 32) { throw new ArgumentOutOfRangeException("nonce"); } if (issuedUnixSeconds < 0 || deadlineUnixSeconds < issuedUnixSeconds) { throw new ArgumentOutOfRangeException("issuedUnixSeconds"); } _nonce = (byte[])nonce.Clone(); IssuedUnixSeconds = issuedUnixSeconds; DeadlineUnixSeconds = deadlineUnixSeconds; } } internal sealed class AdmissionReport { internal string RequestId { get; } internal string ClientVersion { get; } internal long CapturedUnixSeconds { get; } internal long IssuedUnixSeconds { get; } internal string ProfileDigest { get; } internal string NonceBinding { get; } internal IReadOnlyList<AdmissionPluginEvidence> Plugins { get; } internal AdmissionReport(string requestId, string clientVersion, long capturedUnixSeconds, long issuedUnixSeconds, string profileDigest, string nonceBinding, IList<AdmissionPluginEvidence> plugins) { RequestId = AdmissionValidation.RequireLowerHex(requestId, 32, "requestId"); ClientVersion = AdmissionValidation.RequireAtom(clientVersion, 1, 32, "clientVersion"); if (capturedUnixSeconds < 0 || issuedUnixSeconds < 0) { throw new ArgumentOutOfRangeException("capturedUnixSeconds"); } ProfileDigest = AdmissionValidation.RequireLowerHex(profileDigest, 64, "profileDigest"); NonceBinding = AdmissionValidation.RequireLowerHex(nonceBinding, 64, "nonceBinding"); if (plugins == null || plugins.Count > 512) { throw new ArgumentOutOfRangeException("plugins"); } CapturedUnixSeconds = capturedUnixSeconds; IssuedUnixSeconds = issuedUnixSeconds; Plugins = new ReadOnlyCollection<AdmissionPluginEvidence>(new List<AdmissionPluginEvidence>(plugins)); } } internal sealed class AdmissionDecisionMessage { internal string RequestId { get; } internal bool Accepted { get; } internal bool ResumeHandshake { get; } internal string ReasonCode { get; } internal long PolicySequence { get; } internal string PolicyProfile { get; } internal long IssuedUnixSeconds { get; } internal AdmissionDecisionMessage(string requestId, bool accepted, bool resumeHandshake, string reasonCode, long policySequence, string policyProfile, long issuedUnixSeconds) { RequestId = AdmissionValidation.RequireLowerHex(requestId, 32, "requestId"); ReasonCode = AdmissionValidation.RequireReason(reasonCode, "reasonCode"); if (policySequence < 0 || issuedUnixSeconds < 0) { throw new ArgumentOutOfRangeException("policySequence"); } if (!string.IsNullOrEmpty(policyProfile)) { AdmissionValidation.RequireAtom(policyProfile, 1, 64, "policyProfile"); } if (resumeHandshake && !accepted) { throw new ArgumentException("Only an accepted decision may resume the native handshake.", "resumeHandshake"); } Accepted = accepted; ResumeHandshake = resumeHandshake; PolicySequence = policySequence; PolicyProfile = policyProfile ?? string.Empty; IssuedUnixSeconds = issuedUnixSeconds; } } internal static class AdmissionValidation { internal static string RequireAtom(string value, int minimum, int maximum, string parameterName) { if (!IsAtom(value, minimum, maximum)) { throw new ArgumentException("Value is not a canonical ASCII atom.", parameterName); } return value; } internal static bool IsAtom(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 string RequireLowerHex(string value, int length, string parameterName) { if (!IsLowerHex(value, length)) { throw new ArgumentException("Value is not canonical lowercase hexadecimal.", parameterName); } return value; } internal static bool IsLowerHex(string value, int length) { if (value == null || value.Length != length) { return false; } foreach (char c in value) { if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) { return false; } } return true; } internal static string RequireReason(string value, string parameterName) { if (!IsReason(value)) { throw new ArgumentException("Reason code is not canonical.", parameterName); } return value; } internal static bool IsReason(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; } } internal static class AdmissionProfileCanonicalizer { internal const int MaximumCanonicalBytes = 262144; private const string Header = "RUNIC-SENTINEL-CLIENT-PROFILE/1\n"; internal static bool TryCreate(IEnumerable<AdmissionPluginEvidence> source, long capturedUnixSeconds, out AdmissionClientProfile profile, out string failure) { profile = null; failure = string.Empty; if (source == null || capturedUnixSeconds < 0) { failure = "profile-missing"; return false; } List<AdmissionPluginEvidence> list = new List<AdmissionPluginEvidence>(); foreach (AdmissionPluginEvidence item in source) { if (item == null) { failure = "profile-entry-null"; return false; } if (list.Count >= 512) { failure = "profile-plugin-cap"; return false; } list.Add(item); } list.Sort((AdmissionPluginEvidence left, AdmissionPluginEvidence right) => string.CompareOrdinal(left.Id, right.Id)); string a = null; StringBuilder stringBuilder = new StringBuilder("RUNIC-SENTINEL-CLIENT-PROFILE/1\n", Math.Min(32768, 64 + list.Count * 160)); foreach (AdmissionPluginEvidence item2 in list) { if (string.Equals(a, item2.Id, StringComparison.Ordinal)) { failure = "profile-plugin-duplicate"; return false; } a = item2.Id; stringBuilder.Append(item2.Id).Append('|').Append(item2.Version) .Append('|') .Append(item2.Sha256) .Append('\n'); if (stringBuilder.Length > 262144) { failure = "profile-size"; return false; } } byte[] bytes = Encoding.UTF8.GetBytes(stringBuilder.ToString()); if (bytes.Length > 262144) { failure = "profile-size"; return false; } string digest; using (SHA256 sHA = SHA256.Create()) { digest = Hex(sHA.ComputeHash(bytes)); } profile = new AdmissionClientProfile(capturedUnixSeconds, digest, list); return true; } internal static string Hex(byte[] bytes) { if (bytes == null) { throw new ArgumentNullException("bytes"); } StringBuilder stringBuilder = new StringBuilder(bytes.Length * 2); for (int i = 0; i < bytes.Length; i++) { stringBuilder.Append(bytes[i].ToString("x2")); } return stringBuilder.ToString(); } internal static bool FixedTimeEquals(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; } } internal enum AdmissionMessageKind : byte { Challenge = 1, Report, Decision } internal static class AdmissionProtocolV2 { internal const string DirectRpcName = "chazman.RunicSentinel.Admission.v2"; internal const int WireSchema = 2; internal const int MaximumPlugins = 512; internal const int MaximumFrameBytes = 262144; internal const int NonceBytes = 32; internal const int RequestIdHexLength = 32; internal const long MaximumClockSkewSeconds = 60L; internal const long MaximumMessageAgeSeconds = 300L; internal const long MaximumChallengeLifetimeSeconds = 120L; private const int Magic = 843141970; private const int Terminal = 843337285; private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); internal static AdmissionChallenge CreateChallenge(long nowUnixSeconds, long lifetimeSeconds) { if (nowUnixSeconds < 0 || lifetimeSeconds < 1 || lifetimeSeconds > 120 || nowUnixSeconds > long.MaxValue - lifetimeSeconds) { throw new ArgumentOutOfRangeException("lifetimeSeconds"); } byte[] array = new byte[32]; byte[] array2 = new byte[16]; using (RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create()) { randomNumberGenerator.GetBytes(array); randomNumberGenerator.GetBytes(array2); } return new AdmissionChallenge(AdmissionProfileCanonicalizer.Hex(array2), array, nowUnixSeconds, nowUnixSeconds + lifetimeSeconds); } internal static bool IsChallengeCurrent(AdmissionChallenge challenge, long nowUnixSeconds, out string failure) { failure = string.Empty; if (challenge == null || nowUnixSeconds < 0) { failure = "challenge-missing"; return false; } if (challenge.DeadlineUnixSeconds < challenge.IssuedUnixSeconds || challenge.DeadlineUnixSeconds - challenge.IssuedUnixSeconds > 120) { failure = "challenge-lifetime"; return false; } if (challenge.IssuedUnixSeconds < nowUnixSeconds - 300 || challenge.IssuedUnixSeconds > nowUnixSeconds + 60 || nowUnixSeconds > challenge.DeadlineUnixSeconds) { failure = "challenge-stale"; return false; } return true; } internal static bool TryValidateReport(AdmissionChallenge challenge, AdmissionReport report, long receivedUnixSeconds, out AdmissionClientProfile profile, out string failure) { profile = null; failure = string.Empty; if (!IsChallengeCurrent(challenge, receivedUnixSeconds, out failure)) { return false; } if (report == null || !AdmissionProfileCanonicalizer.FixedTimeEquals(challenge.RequestId, report.RequestId)) { failure = "report-request-mismatch"; return false; } if (report.IssuedUnixSeconds < receivedUnixSeconds - 300 || report.IssuedUnixSeconds > receivedUnixSeconds + 60 || report.CapturedUnixSeconds > receivedUnixSeconds + 60) { failure = "report-stale"; return false; } if (!AdmissionProfileCanonicalizer.TryCreate(report.Plugins, report.CapturedUnixSeconds, out profile, out failure)) { return false; } if (!AdmissionProfileCanonicalizer.FixedTimeEquals(profile.Digest, report.ProfileDigest)) { profile = null; failure = "report-digest-mismatch"; return false; } if (!TryComputeNonceBinding(challenge.RequestId, challenge.Nonce, profile.Digest, out var binding) || !AdmissionProfileCanonicalizer.FixedTimeEquals(binding, report.NonceBinding)) { profile = null; failure = "report-binding-mismatch"; return false; } return true; } internal static bool TryComputeNonceBinding(string requestId, byte[] nonce, string profileDigest, out string binding) { binding = string.Empty; if (!AdmissionValidation.IsLowerHex(requestId, 32) || nonce == null || nonce.Length != 32 || !AdmissionValidation.IsLowerHex(profileDigest, 64)) { return false; } string s = "RUNIC-SENTINEL-ADMISSION-BINDING/2\n" + requestId + "\n" + AdmissionProfileCanonicalizer.Hex(nonce) + "\n" + profileDigest + "\n"; using (SHA256 sHA = SHA256.Create()) { binding = AdmissionProfileCanonicalizer.Hex(sHA.ComputeHash(Encoding.ASCII.GetBytes(s))); } return true; } internal static byte[] EncodeChallenge(AdmissionChallenge value) { if (value == null) { throw new ArgumentNullException("value"); } return Encode(AdmissionMessageKind.Challenge, delegate(BinaryWriter writer) { WriteString(writer, value.RequestId, 32); byte[] nonce = value.Nonce; writer.Write(nonce.Length); writer.Write(nonce); writer.Write(value.IssuedUnixSeconds); writer.Write(value.DeadlineUnixSeconds); }); } internal static byte[] EncodeDecision(AdmissionDecisionMessage value) { if (value == null) { throw new ArgumentNullException("value"); } return Encode(AdmissionMessageKind.Decision, delegate(BinaryWriter writer) { WriteString(writer, value.RequestId, 32); writer.Write(value.Accepted ? ((byte)1) : ((byte)0)); writer.Write(value.ResumeHandshake ? ((byte)1) : ((byte)0)); WriteString(writer, value.ReasonCode, 96); writer.Write(value.PolicySequence); WriteString(writer, value.PolicyProfile, 64); writer.Write(value.IssuedUnixSeconds); }); } internal static bool TryGetKind(byte[] bytes, out AdmissionMessageKind kind, out string failure) { kind = (AdmissionMessageKind)0; failure = string.Empty; try { using MemoryStream input = NewReadStream(bytes); using BinaryReader reader = new BinaryReader(input, StrictUtf8); if (!ReadHeader(reader, out kind, out failure)) { return false; } return true; } catch { kind = (AdmissionMessageKind)0; failure = "frame-malformed"; return false; } } internal static bool TryDecodeReport(byte[] bytes, out AdmissionReport value, out string failure) { value = null; failure = string.Empty; try { using MemoryStream memoryStream = NewReadStream(bytes); using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8); if (!ReadExpectedHeader(binaryReader, AdmissionMessageKind.Report, out failure) || !TryReadString(binaryReader, memoryStream, 32, out var value2) || !TryReadString(binaryReader, memoryStream, 32, out var value3)) { failure = "report-shape"; return false; } long num = binaryReader.ReadInt64(); long num2 = binaryReader.ReadInt64(); if (!TryReadString(binaryReader, memoryStream, 64, out var value4) || !TryReadString(binaryReader, memoryStream, 64, out var value5)) { failure = "report-shape"; return false; } int num3 = binaryReader.ReadInt32(); if (num3 < 0 || num3 > 512) { failure = "report-plugin-cap"; return false; } List<AdmissionPluginEvidence> list = new List<AdmissionPluginEvidence>(num3); string text = null; for (int i = 0; i < num3; i++) { if (!TryReadString(binaryReader, memoryStream, 128, out var value6) || !TryReadString(binaryReader, memoryStream, 64, out var value7) || !TryReadString(binaryReader, memoryStream, 64, out var value8) || !AdmissionValidation.IsAtom(value6, 1, 128) || !AdmissionValidation.IsAtom(value7, 1, 64) || !AdmissionValidation.IsLowerHex(value8, 64) || (text != null && string.CompareOrdinal(text, value6) >= 0)) { failure = "report-plugin-shape"; return false; } text = value6; list.Add(new AdmissionPluginEvidence(value6, value7, value8)); } if (!AdmissionValidation.IsLowerHex(value2, 32) || !AdmissionValidation.IsAtom(value3, 1, 32) || num < 0 || num2 < 0 || !AdmissionValidation.IsLowerHex(value4, 64) || !AdmissionValidation.IsLowerHex(value5, 64) || !ReadTerminal(binaryReader, memoryStream)) { failure = "report-shape"; return false; } value = new AdmissionReport(value2, value3, num, num2, value4, value5, list); return true; } catch { value = null; failure = "report-malformed"; return false; } } private static byte[] Encode(AdmissionMessageKind kind, Action<BinaryWriter> body) { using MemoryStream memoryStream = new MemoryStream(); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, StrictUtf8); binaryWriter.Write(843141970); binaryWriter.Write(2); binaryWriter.Write((byte)kind); body(binaryWriter); binaryWriter.Write(843337285); binaryWriter.Flush(); if (memoryStream.Length <= 0 || memoryStream.Length > 262144) { throw new InvalidDataException("Admission frame exceeds its wire bound."); } return memoryStream.ToArray(); } private static MemoryStream NewReadStream(byte[] bytes) { if (bytes == null || bytes.Length == 0 || bytes.Length > 262144) { throw new InvalidDataException("Admission frame size is invalid."); } return new MemoryStream(bytes, writable: false); } private static bool ReadHeader(BinaryReader reader, out AdmissionMessageKind kind, out string failure) { kind = (AdmissionMessageKind)0; failure = string.Empty; if (reader.ReadInt32() != 843141970 || reader.ReadInt32() != 2) { failure = "frame-header"; return false; } kind = (AdmissionMessageKind)reader.ReadByte(); if ((int)kind < 1 || (int)kind > 3) { kind = (AdmissionMessageKind)0; failure = "frame-kind"; return false; } return true; } private static bool ReadExpectedHeader(BinaryReader reader, AdmissionMessageKind expected, out string failure) { if (!ReadHeader(reader, out var kind, out failure)) { return false; } if (kind == expected) { return true; } failure = "frame-kind"; return false; } private static void WriteString(BinaryWriter writer, string value, int maximumCharacters) { if (value == null || value.Length > maximumCharacters) { throw new InvalidDataException("Admission string exceeds its bound."); } byte[] bytes = StrictUtf8.GetBytes(value); if (bytes.Length > maximumCharacters) { throw new InvalidDataException("Admission string exceeds its byte bound."); } writer.Write(bytes.Length); writer.Write(bytes); } private static bool TryReadString(BinaryReader reader, MemoryStream stream, int maximumBytes, out string value) { value = string.Empty; int num = reader.ReadInt32(); if (num < 0 || num > maximumBytes || Remaining(stream) < num) { return false; } byte[] array = reader.ReadBytes(num); if (array.Length != num) { return false; } value = StrictUtf8.GetString(array); return value.Length <= maximumBytes; } private static bool ReadTerminal(BinaryReader reader, MemoryStream stream) { if (Remaining(stream) == 4 && reader.ReadInt32() == 843337285) { return Remaining(stream) == 0; } return false; } private static long Remaining(MemoryStream stream) { return stream.Length - stream.Position; } } } namespace RunicSentinel.Runtime { internal sealed class SentinelAdminControl : IDisposable { private sealed class ServerConnection { internal ZNetPeer Peer { get; } internal ZRpc Rpc { get; } internal long Ordinal { get; } internal ServerConnection(ZNetPeer peer, ZRpc rpc, long ordinal) { Peer = peer; Rpc = rpc; Ordinal = ordinal; } } 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 MaximumReplayEntries = 256; private const int MaximumTrackedPeers = 64; private static readonly long ReplayLifetimeTicks = TimeSpan.FromMinutes(1.0).Ticks; private readonly SentinelRuntime _runtime; private readonly SentinelManagedPolicyService _managed; private readonly SentinelOperatorCommands _commands; private readonly Dictionary<string, CachedResponse> _cache = new Dictionary<string, CachedResponse>(StringComparer.Ordinal); private readonly Queue<string> _cacheOrder = new Queue<string>(); private readonly Dictionary<ZRpc, ServerConnection> _serverConnections = new Dictionary<ZRpc, ServerConnection>(); private ZNet _network; private long _nextConnectionOrdinal; 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; } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { if ((Object)(object)_network != (Object)null) { ClearConnections("The authoritative server administrator channel disconnected."); _network = null; } } else { EnsureNetwork(instance); if (instance.IsServer()) { TickServer(instance); } else { ClearConnections("Runic Sentinel Server is inert on a non-authoritative client."); _network = null; } } long ticks = DateTime.UtcNow.Ticks; ExpireCache(ticks); } private void EnsureNetwork(ZNet network) { if (network != _network) { ClearConnections("The authoritative server administrator channel changed."); _network = network; } } private void TickServer(ZNet network) { List<ZNetPeer> list; try { list = network.GetPeers(); } catch { list = null; } Dictionary<ZRpc, ZNetPeer> dictionary = new Dictionary<ZRpc, ZNetPeer>(); int num = 0; if (list != null) { foreach (ZNetPeer item in list) { if (num++ >= 64) { break; } ZRpc val = item?.m_rpc; if (val != null && IsReady(item) && !dictionary.ContainsKey(val)) { dictionary.Add(val, item); } } } foreach (ZRpc item2 in new List<ZRpc>(_serverConnections.Keys)) { if (!dictionary.TryGetValue(item2, out var value) || !_serverConnections.TryGetValue(item2, out var value2) || value2.Peer != value || value.m_rpc != item2) { RemoveServerConnection(item2); } } foreach (KeyValuePair<ZRpc, ZNetPeer> item3 in dictionary) { if (!_serverConnections.ContainsKey(item3.Key) && _serverConnections.Count < 64) { try { item3.Key.Register<ZPackage>("runic.sentinel.admin.request.v1", (Action<ZRpc, ZPackage>)ReceiveRequest); } catch { continue; } long ordinal = ((_nextConnectionOrdinal == long.MaxValue) ? long.MaxValue : (++_nextConnectionOrdinal)); _serverConnections.Add(item3.Key, new ServerConnection(item3.Value, item3.Key, ordinal)); } } } private void ReceiveRequest(ZRpc rpc, ZPackage package) { ZNet instance = ZNet.instance; if (_disposed || (Object)(object)instance == (Object)null || instance != _network || !instance.IsServer() || rpc == null || !_serverConnections.TryGetValue(rpc, out var value)) { return; } ZNetPeer val = FindExactReadyPeer(instance, rpc); if (val == null || val != value.Peer || val.m_rpc != value.Rpc) { return; } if (!TryReadRequest(package, out var id, out var action, out var payload, out var issued)) { SendResponse(value, string.Empty, accepted: false, "Malformed administrator request.", Array.Empty<byte>()); return; } long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (issued < num - 30 || issued > num + 30) { SendResponse(value, id, accepted: false, "Administrator request expired.", Array.Empty<byte>()); return; } string key = value.Ordinal.ToString(CultureInfo.InvariantCulture) + ":" + id; byte[] array = Digest(action, payload, issued); ExpireCache(DateTime.UtcNow.Ticks); string authority; string subject; if (_cache.TryGetValue(key, out var value2)) { if (!Fixed(value2.RequestDigest, array)) { SendResponse(value, id, accepted: false, "Administrator request identity was reused.", Array.Empty<byte>()); } else { SendResponse(value, id, value2.Accepted, value2.Reason, value2.Payload); } } else if (!SentinelTransportIdentity.TryResolvePeer(val, out authority, out subject)) { SendResponse(value, id, accepted: false, "Authenticated backend identity is unavailable.", Array.Empty<byte>()); } else { Execute(authority, subject, action, payload, out var accepted, out var response, out var reason); Cache(key, array, accepted, reason, response); SendResponse(value, id, accepted, reason, response); } } private void Execute(string authority, string subject, string action, byte[] payload, out bool accepted, out byte[] response, out string reason) { accepted = false; response = Array.Empty<byte>(); reason = "Administrator access denied."; if (_runtime.IsBanned(authority, subject)) { reason = "This account is banned."; } else { if (!_runtime.IsAdministrator(authority, subject)) { return; } try { SentinelAdminDocument value; if (action == "status" && payload.Length == 0) { response = SentinelAdminProtocol.Encode(_managed.CreateDocument("Authenticated by " + authority + " backend identity.")); } else if (action == "apply" && SentinelAdminProtocol.TryDecode(payload, out value)) { response = SentinelAdminProtocol.EncodeMessage(_managed.Apply(value, authority, subject)); } else { if (!(action == "tool") || !SentinelAdminProtocol.TryDecodeTool(payload, out var tool)) { reason = "Administrator operation is invalid."; return; } response = SentinelAdminProtocol.EncodeMessage(RunToolCore(tool)); } accepted = true; reason = "ok"; } catch (Exception ex) { reason = Bounded(ex.Message); } } } private string RunToolCore(string tool) { return tool switch { "report" => "Support report created: " + _commands.WriteReport(), "networks" => "Network map created: " + _commands.WriteReport(includeNetworks: true), "backup" => "Verified backup created: " + SentinelTransitionBackup.CreateVerifiedBackupNow("runic-sentinel-admin-tool"), _ => throw new InvalidOperationException("Unknown administrator tool."), }; } private 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 void SendResponse(ServerConnection connection, string id, bool accepted, string reason, byte[] payload) { ZNet network = _network; if (_disposed || (Object)(object)network == (Object)null || !network.IsServer() || connection == null || FindExactReadyPeer(network, connection.Rpc) != connection.Peer) { return; } ZPackage val = WriteResponse(id, accepted, reason, payload); if (val.Size() > 184320) { return; } try { connection.Rpc.Invoke("runic.sentinel.admin.response.v1", new object[1] { val }); } catch { } } private static ZPackage WriteResponse(string id, bool accepted, string reason, byte[] payload) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(1); val.Write(id ?? string.Empty); val.Write(accepted); val.Write(Bounded(reason)); val.Write(payload ?? Array.Empty<byte>()); val.Write(1369914905); return val; } private static ZNetPeer FindExactReadyPeer(ZNet network, ZRpc rpc) { if ((Object)(object)network == (Object)null || rpc == null || !network.IsServer()) { return null; } List<ZNetPeer> peers; try { peers = network.GetPeers(); } catch { return null; } if (peers == null) { return null; } int num = 0; foreach (ZNetPeer item in peers) { if (num++ >= 64) { break; } if (item != null && item.m_rpc == rpc && IsReady(item)) { return item; } } return null; } private static bool IsReady(ZNetPeer peer) { try { return peer != null && peer.IsReady(); } catch { return false; } } private void RemoveServerConnection(ZRpc rpc) { if (rpc == null || !_serverConnections.Remove(rpc)) { return; } try { rpc.Unregister("runic.sentinel.admin.request.v1"); } catch { } } private void ClearServerConnections() { foreach (ZRpc item in new List<ZRpc>(_serverConnections.Keys)) { try { if (item != null) { item.Unregister("runic.sentinel.admin.request.v1"); } } catch { } } _serverConnections.Clear(); } private void ClearConnections(string reason) { ClearServerConnections(); } private void Cache(string key, byte[] digest, bool accepted, string reason, byte[] payload) { while (_cache.Count >= 256 && _cacheOrder.Count != 0) { _cache.Remove(_cacheOrder.Dequeue()); } _cache[key] = new CachedResponse { RequestDigest = digest, Accepted = accepted, Reason = Bounded(reason), Payload = (byte[])(payload ?? Array.Empty<byte>()).Clone(), Expires = DateTime.UtcNow.Ticks + ReplayLifetimeTicks }; _cacheOrder.Enqueue(key); } private void ExpireCache(long now) { while (_cacheOrder.Count != 0) { string key = _cacheOrder.Peek(); if (!_cache.TryGetValue(key, out var value) || value.Expires <= now) { _cacheOrder.Dequeue(); _cache.Remove(key); continue; } break; } } private static byte[] Digest(string action, byte[] payload, long issued) { byte[] bytes = Encoding.UTF8.GetBytes(action + "\n" + issued.ToString(CultureInfo.InvariantCulture) + "\n"); byte[] array = new byte[bytes.Length + payload.Length]; Buffer.BlockCopy(bytes, 0, array, 0, bytes.Length); Buffer.BlockCopy(payload, 0, array, bytes.Length, payload.Length); using SHA256 sHA = SHA256.Create(); return sHA.ComputeHash(array); } private static bool Fixed(byte[] left, byte[] right) { if (left != null && right != null && left.Length == right.Length) { return CryptographicOperations.FixedTimeEquals(left, right); } return false; } private static bool CanonicalId(string id) { Guid result; if (id != null && id.Length == 32) { return Guid.TryParseExact(id, "N", out result); } return false; } private static string Bounded(string reason) { if (!string.IsNullOrWhiteSpace(reason)) { if (reason.Length > 512) { return reason.Substring(0, 512); } return reason; } return "Administrator operation failed."; } public void Dispose() { _disposed = true; ClearConnections("Administrator control stopped."); _cache.Clear(); _cacheOrder.Clear(); _network = null; } } internal sealed class SentinelAdminDocument { internal long Sequence; internal string Profile = "runic-suite"; internal string ExpiresUnixSeconds = "0"; internal string UnknownMods = "Forbidden"; internal string RequiredMods = string.Empty; internal string OptionalMods = string.Empty; internal string GrayListMods = string.Empty; internal string ForbiddenMods = string.Empty; internal string Administrators = string.Empty; internal string BannedUsers = string.Empty; internal string Modules = string.Empty; internal string DetectedProfile = string.Empty; internal string Integrity = string.Empty; internal string LastDenial = string.Empty; internal string AdmissionMode = "Optional"; internal string IntegritySeconds = "15"; internal string VeryHighThreshold = "2"; internal string HighThreshold = "3"; internal string EnforcementWindowSeconds = "60"; internal bool BackupTransitions = true; internal bool ManagedSigningKey; internal string SigningKeyPin = string.Empty; internal string Status = string.Empty; } internal static class SentinelAdminProtocol { internal const int MaximumWireBytes = 122880; private const string Header = "RUNIC-SENTINEL-ADMIN/1\n"; private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); internal static byte[] Encode(SentinelAdminDocument value) { if (value == null) { throw new ArgumentNullException("value"); } Dictionary<string, string> obj = new Dictionary<string, string>(StringComparer.Ordinal) { ["sequence"] = value.Sequence.ToString(CultureInfo.InvariantCulture), ["profile"] = value.Profile, ["expires"] = value.ExpiresUnixSeconds, ["unknown"] = value.UnknownMods, ["required"] = value.RequiredMods, ["optional"] = value.OptionalMods, ["gray"] = value.GrayListMods, ["forbidden"] = value.ForbiddenMods, ["admins"] = value.Administrators, ["bans"] = value.BannedUsers, ["modules"] = value.Modules, ["detected"] = value.DetectedProfile, ["integrity"] = value.Integrity, ["last-denial"] = value.LastDenial, ["admission"] = value.AdmissionMode, ["integrity-seconds"] = value.IntegritySeconds, ["very-high"] = value.VeryHighThreshold, ["high"] = value.HighThreshold, ["window"] = value.EnforcementWindowSeconds, ["backup"] = (value.BackupTransitions ? "1" : "0"), ["managed-key"] = (value.ManagedSigningKey ? "1" : "0"), ["key-pin"] = value.SigningKeyPin, ["status"] = value.Status }; StringBuilder stringBuilder = new StringBuilder("RUNIC-SENTINEL-ADMIN/1\n"); foreach (KeyValuePair<string, string> item in obj) { stringBuilder.Append(item.Key).Append('=').Append(Convert.ToBase64String(StrictUtf8.GetBytes(item.Value ?? string.Empty))) .Append('\n'); } byte[] bytes = StrictUtf8.GetBytes(stringBuilder.ToString()); if (bytes.Length > 122880) { throw new InvalidDataException("admin-document-too-large"); } return bytes; } internal static bool TryDecode(byte[] bytes, out SentinelAdminDocument value) { value = null; if (bytes == null || bytes.Length == 0 || bytes.Length > 122880) { return false; } string text; try { text = StrictUtf8.GetString(bytes); } catch { return false; } if (!text.StartsWith("RUNIC-SENTINEL-ADMIN/1\n", StringComparison.Ordinal) || text.IndexOf('\r') >= 0 || !text.EndsWith("\n", StringComparison.Ordinal)) { return false; } string[] array = text.Split('\n'); Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal); for (int i = 1; i < array.Length - 1; i++) { int num = array[i].IndexOf('='); if (num <= 0 || !dictionary.TryAdd(array[i].Substring(0, num), Decode(array[i].Substring(num + 1)))) { return false; } } if (!TryLong(dictionary, "sequence", out var result)) { return false; } value = new SentinelAdminDocument { Sequence = result, Profile = Get(dictionary, "profile"), ExpiresUnixSeconds = Get(dictionary, "expires"), UnknownMods = Get(dictionary, "unknown"), RequiredMods = Get(dictionary, "required"), OptionalMods = Get(dictionary, "optional"), GrayListMods = Get(dictionary, "gray"), ForbiddenMods = Get(dictionary, "forbidden"), Administrators = Get(dictionary, "admins"), BannedUsers = Get(dictionary, "bans"), Modules = Get(dictionary, "modules"), DetectedProfile = Get(dictionary, "detected"), Integrity = Get(dictionary, "integrity"), LastDenial = Get(dictionary, "last-denial"), AdmissionMode = Get(dictionary, "admission"), IntegritySeconds = Get(dictionary, "integrity-seconds"), VeryHighThreshold = Get(dictionary, "very-high"), HighThreshold = Get(dictionary, "high"), EnforcementWindowSeconds = Get(dictionary, "window"), BackupTransitions = (Get(dictionary, "backup") == "1"), ManagedSigningKey = (Get(dictionary, "managed-key") == "1"), SigningKeyPin = Get(dictionary, "key-pin"), Status = Get(dictionary, "status") }; return true; } internal static byte[] EncodeTool(string tool) { string text = tool ?? string.Empty; if (text != "report" && text != "networks" && text != "backup") { throw new ArgumentException("Unknown admin tool.", "tool"); } return StrictUtf8.GetBytes("RUNIC-SENTINEL-ADMIN-TOOL/1\n" + text + "\n"); } internal static bool TryDecodeTool(byte[] bytes, out string tool) { tool = string.Empty; if (bytes == null || bytes.Length > 128) { return false; } string text; try { text = StrictUtf8.GetString(bytes); } catch { return false; } if (!text.StartsWith("RUNIC-SENTINEL-ADMIN-TOOL/1\n", StringComparison.Ordinal) || !text.EndsWith("\n", StringComparison.Ordinal)) { return false; } tool = text.Substring("RUNIC-SENTINEL-ADMIN-TOOL/1\n".Length, text.Length - "RUNIC-SENTINEL-ADMIN-TOOL/1\n".Length - 1); if (!(tool == "report") && !(tool == "networks")) { return tool == "backup"; } return true; } internal static byte[] EncodeMessage(string value) { byte[] bytes = StrictUtf8.GetBytes(value ?? string.Empty); if (bytes.Length > 4096) { throw new InvalidDataException("admin-message-too-large"); } return bytes; } internal static string DecodeMessage(byte[] bytes) { if (bytes == null || bytes.Length > 4096) { return "invalid-response"; } try { return StrictUtf8.GetString(bytes); } catch { return "invalid-response"; } } private static string Decode(string value) { try { return StrictUtf8.GetString(Convert.FromBase64String(value)); } catch { return null; } } private static string Get(IDictionary<string, string> values, string key) { if (!values.TryGetValue(key, out var value) || value == null) { return string.Empty; } return value; } private static bool TryLong(IDictionary<string, string> values, string key, out long result) { if (long.TryParse(Get(values, key), NumberStyles.None, CultureInfo.InvariantCulture, out result)) { return result >= 0; } return false; } } internal static class SentinelDraftExporter { internal const string FileName = "RunicSentinel.current-profile.json"; internal static void TryWrite(string configRoot, AttestationSnapshot snapshot) { try { if (snapshot != null && !string.IsNullOrEmpty(configRoot)) { string text = Path.Combine(Path.GetFullPath(configRoot), "RunicSentinel.current-profile.json"); string text2 = text + ".tmp"; byte[] bytes = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetBytes(Build(snapshot)); using (FileStream fileStream = new FileStream(text2, FileMode.Create, FileAccess.Write, FileShare.None, 65536, FileOptions.WriteThrough)) { fileStream.Write(bytes, 0, bytes.Length); fileStream.Flush(flushToDisk: true); } if (File.Exists(text)) { File.Replace(text2, text, null); } else { File.Move(text2, text); } } } catch { } } private static string Build(AttestationSnapshot snapshot) { StringBuilder stringBuilder = new StringBuilder(4096 + snapshot.Plugins.Count * 160); stringBuilder.Append("{\n \"profile\": \"runic-suite\",\n \"sequence\": 1,\n \"issued\": ").Append(DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture)).Append(",\n \"expires\": 0,\n \"unknownMods\": \"Forbidden\",\n") .Append(" \"requiredMods\": [\n"); for (int i = 0; i < snapshot.Plugins.Count; i++) { AttestedPlugin attestedPlugin = snapshot.Plugins[i]; stringBuilder.Append(" { \"id\": \"").Append(Json(attestedPlugin.Id)).Append("\", \"version\": \"") .Append(Json(attestedPlugin.Version)) .Append("\", \"sha256\": \"") .Append(attestedPlugin.Sha256) .Append("\" }") .Append((i + 1 == snapshot.Plugins.Count) ? "\n" : ",\n"); } stringBuilder.Append(" ],\n \"optionalMods\": [],\n \"grayListMods\": [],\n").Append(" \"forbiddenMods\": [],\n \"modules\": [],\n").Append(" \"administrators\": [],\n \"bannedUsers\": []\n}\n"); return stringBuilder.ToString(); } private static string Json(string value) { StringBuilder stringBuilder = new StringBuilder(value?.Length ?? 0); string text = value ?? string.Empty; foreach (char c in text) { switch (c) { case '\\': stringBuilder.Append("\\\\"); continue; case '"': stringBuilder.Append("\\\""); continue; case '\b': stringBuilder.Append("\\b"); continue; case '\f': stringBuilder.Append("\\f"); continue; case '\n': stringBuilder.Append("\\n"); continue; case '\r': stringBuilder.Append("\\r"); continue; case '\t': stringBuilder.Append("\\t"); continue; } if (c < ' ') { StringBuilder stringBuilder2 = stringBuilder.Append("\\u"); int num = c; stringBuilder2.Append(num.ToString("x4")); } else { stringBuilder.Append(c); } } return stringBuilder.ToString(); } } internal sealed class SentinelEnforcementRuntime : IDisposable { private sealed class EscalationState { internal long Started { get; } internal int High { get; set; } internal int VeryHigh { get; set; } internal EscalationState(long started) { Started = started; } } private const int MaximumTrackedPeers = 256; private readonly object _gate = new object(); private readonly SentinelRuntime _runtime; private readonly ISentinelEvidenceProviderLease _evidence; private readonly Dictionary<long, EscalationState> _states = new Dictionary<long, EscalationState>(); private bool _disposed; internal SentinelEnforcementRuntime(SentinelRuntime runtime) { _runtime = runtime ?? throw new ArgumentNullException("runtime"); _evidence = runtime.Evidence.RegisterProvider("runic.sentinel.enforcement"); } internal bool ReportRejectedServerRequest(string sourceModuleId, long peerId, string actor, string rule, string correlationId, FindingConfidence confidence, string detail) { if (sourceModuleId != "runic.portals" || peerId == 0L || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return false; } bool flag = false; long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); lock (_gate) { if (_disposed) { return false; } Prune(num); long num2 = Math.Max(10, Math.Min(600, SentinelConfig.EnforcementWindowSeconds?.Value ?? 60)); int num3 = Math.Max(1, Math.Min(10, SentinelConfig.VeryHighDisconnectCount?.Value ?? 2)); int num4 = Math.Max(1, Math.Min(20, SentinelConfig.HighDisconnectCount?.Value ?? 3)); if (!_states.TryGetValue(peerId, out var value) || num - value.Started > num2) { value = new EscalationState(num); } if (confidence >= FindingConfidence.High) { value.High++; } if (confidence >= FindingConfidence.VeryHigh) { value.VeryHigh++; } flag = confidence == FindingConfidence.Conclusive || value.VeryHigh >= num3 || value.High >= num4; _states[peerId] = value; _evidence.Sink.TryAppend(Safe(actor, "peer:" + peerId), Safe(rule, "security-violation"), Safe(correlationId, Guid.NewGuid().ToString("N")), confidence, flag ? EnforcementAction.Disconnect : EnforcementAction.Cancel, Safe(detail, "request-denied"), out var _); } if (!flag) { return true; } try { ZNetPeer peer = ZNet.instance.GetPeer(peerId); if (peer != null && peer.m_uid == peerId && peer.IsReady()) { ZNet.instance.Disconnect(peer); } } catch { } return true; } private void Prune(long now) { long num = Math.Max(10, Math.Min(600, SentinelConfig.EnforcementWindowSeconds?.Value ?? 60)); List<long> list = new List<long>(); foreach (KeyValuePair<long, EscalationState> state in _states) { if (now - state.Value.Started > num) { list.Add(state.Key); } } foreach (long item in list) { _states.Remove(item); } if (_states.Count < 256) { return; } long key = 0L; long num2 = long.MaxValue; foreach (KeyValuePair<long, EscalationState> state2 in _states) { if (state2.Value.Started < num2) { num2 = state2.Value.Started; key = state2.Key; } } _states.Remove(key); } private static string Safe(string value, string fallback) { string text = (string.IsNullOrEmpty(value) ? fallback : value); if (text.Length > 128) { text = text.Substring(0, 128); } return text; } public void Dispose() { lock (_gate) { if (_disposed) { return; } _disposed = true; _states.Clear(); } try { _evidence.Dispose(); } catch { } } } internal sealed class SentinelFlightRecorder : IDisposable { internal const long MaximumFileBytes = 524288L; private static readonly UTF8Encoding Utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); private static readonly byte[] Header = Utf8.GetBytes("RUNIC-SENTINEL-FLIGHT/1\n"); private readonly object _gate = new object(); private readonly EvidenceLedger _ledger; private readonly ManualLogSource _log; private readonly string _activePath; private readonly string _previousPath; private bool _disposed; private bool _faultLogged; internal string ActivePath => _activePath; internal SentinelFlightRecorder(EvidenceLedger ledger, ManualLogSource log, string configRoot) { _ledger = ledger ?? throw new ArgumentNullException("ledger"); _log = log; string path = Path.Combine(Path.GetFullPath(configRoot), "RunicSentinel", "flight-recorder"); _activePath = Path.Combine(path, "security-current.log"); _previousPath = Path.Combine(path, "security-previous.log"); _ledger.Accepted += OnAccepted; } private void OnAccepted(SecurityEvidence evidence) { if (evidence == null) { return; } byte[] bytes = Utf8.GetBytes(Encode(evidence)); lock (_gate) { if (_disposed) { return; } try { Directory.CreateDirectory(Path.GetDirectoryName(_activePath)); long num = (File.Exists(_activePath) ? new FileInfo(_activePath).Length : 0); long num2 = (long)bytes.Length + (long)((num == 0L) ? Header.Length : 0); if (num + num2 > 524288) { if (File.Exists(_previousPath)) { File.Delete(_previousPath); } if (File.Exists(_activePath)) { File.Move(_activePath, _previousPath); } num = 0L; } using FileStream fileStream = new FileStream(_activePath, FileMode.Append, FileAccess.Write, FileShare.Read, 4096, FileOptions.WriteThrough); if (num == 0L) { fileStream.Write(Header, 0, Header.Length); } fileStream.Write(bytes, 0, bytes.Length); fileStream.Flush(flushToDisk: true); } catch (Exception ex) { if (!_faultLogged) { _faultLogged = true; ManualLogSource log = _log; if (log != null) { log.LogWarning((object)("Sentinel flight-recorder write failed; enforcement remains active: " + ex.GetType().Name + ".")); } } } } } private static string Encode(SecurityEvidence value) { return value.Sequence.ToString(CultureInfo.InvariantCulture) + "|" + value.UnixSeconds.ToString(CultureInfo.InvariantCulture) + "|" + Base64(value.ProviderModuleId) + "|" + Base64(value.Actor) + "|" + Base64(value.Rule) + "|" + Base64(value.CorrelationId) + "|" + ((int)value.Confidence).ToString(CultureInfo.InvariantCulture) + "|" + ((int)value.RequestedAction).ToString(CultureInfo.InvariantCulture) + "|" + ((int)value.EffectiveAction).ToString(CultureInfo.InvariantCulture) + "|" + value.PolicySequence.ToString(CultureInfo.InvariantCulture) + "|" + Base64(value.Detail) + "\n"; } private static string Base64(string value) { return Convert.ToBase64String(Utf8.GetBytes(value ?? string.Empty)); } public void Dispose() { lock (_gate) { if (_disposed) { return; } _disposed = true; } _ledger.Accepted -= OnAccepted; } } internal enum SentinelIntegrityState { Unavailable, MonitorOnly, Ready, Compromised } internal sealed class SentinelIntegritySnapshot { internal SentinelIntegrityState State { get; } internal long CheckedUnixSeconds { get; } internal string ReasonCode { get; } internal string PolicyDigest { get; } internal SentinelIntegritySnapshot(SentinelIntegrityState state, long checkedUnixSeconds, string reasonCode, string policyDigest) { if (!Enum.IsDefined(typeof(SentinelIntegrityState), state) || checkedUnixSeconds < 0) { throw new ArgumentOutOfRangeException("state"); } State = state; CheckedUnixSeconds = checkedUnixSeconds; ReasonCode = (string.IsNullOrEmpty(reasonCode) ? "unavailable" : reasonCode); PolicyDigest = policyDigest ?? string.Empty; } } internal sealed class SentinelManagedPolicyService { private sealed class Rule { internal string Classification { get; } internal string Id { get; } internal string Version { get; } internal string Hash { get; } internal Rule(string classification, string id, string version, string hash) { Classification = classification; Id = id; Version = version; Hash = hash; } } private const string PrivateHeader = "RUNIC-RSA-PRIVATE/1"; private readonly object _gate = new object(); private readonly SentinelRuntime _runtime; private readonly ManualLogSource _log; private readonly string _configRoot; private readonly string _privatePath; private readonly Func<string, string> _backup; internal bool HasManagedKey => File.Exists(_privatePath); internal SentinelManagedPolicyService(SentinelRuntime runtime, ManualLogSource log, string configRoot, Func<string, string> backup) { _runtime = runtime ?? throw new ArgumentNullException("runtime"); _log = log; _configRoot = Path.GetFullPath(configRoot); _privatePath = Path.Combine(_configRoot, "RunicSentinel", "server-private", "RunicSentinel.private.key"); _backup = backup; } internal SentinelAdminDocument CreateDocument(string status = "Ready") { SentinelAdminDocument sentinelAdminDocument = new SentinelAdminDocument { Status = status, AdmissionMode = _runtime.EffectiveRemoteAdmissionMode.ToString(), 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; } AttestationSnapshot snapshot; string status2; string text = ((!_runtime.TryGetCurrent(out snapshot, out status2)) ? ("Snapshot unavailable: " + status2) : string.Join("\n", snapshot.Plugins.Select((AttestedPlugin plugin) => plugin.Id + "|" + plugin.Version + "|" + plugin.Sha256))); AdmissionClientProfile profile; string text2 = (_runtime.TryGetLastRemoteAdmissionProfile(out profile) ? string.Join("\n", profile.Plugins.Select((AdmissionPluginEvidence plugin) => plugin.Id + "|" + plugin.Version + "|" + plugin.Sha256)) : "No client report observed in this process lifetime."); sentinelAdminDocument.DetectedProfile = "SERVER PROFILE (not a client allowlist)\n" + text + "\n\nMOST RECENT CLIENT REPORT\n" + text2; 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 = CreateManagedRsa3072()) { rSAParameters = rSA.ExportParameters(includePrivateParameters: true); } SentinelPolicy policy; SentinelAdminDocument sentinelAdminDocument = (_runtime.TryGetVerifiedPolicy(out policy) ? CreateDocument("Bootstrap") : DefaultDocument(snapshot)); SortedDictionary<string, SentinelAdministratorRole> sortedDictionary = ParseIdentities(sentinelAdminDocument.Administrators, "administrators"); sortedDictionary[authority + ":" + Uri.EscapeDataString(subject)] = new SentinelAdministratorRole(authority, subject); sentinelAdminDocument.Administrators = IdentityLines(sortedDictionary.Values); string text = ApplyCore(sentinelAdminDocument, null, rSAParameters, changingTrustRoot: true); Directory.CreateDirectory(Path.GetDirectoryName(_privatePath)); try { WriteExclusive(_privatePath, EncodePrivate(rSAParameters)); } catch { throw new IOException("managed-key-persistence-failed-after-policy-signing"); } return text + " Initial administrator: " + authority + ":" + subject + "."; } } internal string Apply(SentinelAdminDocument draft, string callerAuthority, string callerSubject) { if (draft == null) { throw new ArgumentNullException("draft"); } lock (_gate) { if (!File.Exists(_privatePath)) { throw new InvalidOperationException("server-managed-signing-key-required"); } if (!_runtime.TryGetVerifiedPolicy(out var policy)) { throw new InvalidOperationException("verified-policy-required"); } if (draft.Sequence != policy.Sequence) { throw new InvalidOperationException("policy-sequence-stale"); } RSAParameters privateParameters = DecodePrivate(File.ReadAllBytes(_privatePath)); return ApplyCore(draft, callerAuthority + ":" + Uri.EscapeDataString(callerSubject), privateParameters, changingTrustRoot: false); } } private string ApplyCore(SentinelAdminDocument draft, string callerKey, RSAParameters privateParameters, bool changingTrustRoot) { ValidateSettings(draft); if (!string.Equals(draft.AdmissionMode, _runtime.EffectiveRemoteAdmissionMode.ToString(), StringComparison.Ordinal)) { throw new InvalidDataException("admission-mode-restart-required"); } 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 = ImportManagedRsa3072(privateParameters)) { 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 remains " + _runtime.EffectiveRemoteAdmissionMode.ToString() + "."; } internal static RSA CreateManagedRsa3072() { RSA rSA = null; try { rSA = RSA.Create(); rSA.KeySize = 3072; if (IsExactRsa3072(rSA, requirePrivate: true)) { return rSA; } } catch { } rSA?.Dispose(); try { rSA = new RSACryptoServiceProvider(3072) { PersistKeyInCsp = false }; if (IsExactRsa3072(rSA, requirePrivate: true)) { return rSA; } } catch { } rSA?.Dispose(); throw new CryptographicException("rsa-3072-unavailable"); } internal static RSA ImportManagedRsa3072(RSAParameters parameters) { RSA rSA = null; try { rSA = RSA.Create(); rSA.ImportParameters(parameters); if (IsExactRsa3072(rSA, requirePrivate: true)) { return rSA; } } catch { } rSA?.Dispose(); try { rSA = new RSACryptoServiceProvider { PersistKeyInCsp = false }; rSA.ImportParameters(parameters); if (IsExactRsa3072(rSA, requirePrivate: true)) { return rSA; } } catch { } rSA?.Dispose(); throw new CryptographicException("managed-key-not-rsa-3072"); } private static bool IsExactRsa3072(RSA rsa, bool requirePrivate) { if (rsa == null || rsa.KeySize != 3072) { return false; } try { RSAParameters rSAParameters = rsa.ExportParameters(requirePrivate); return rSAParameters.Modulus != null && rSAParameters.Modulus.Length == 384 && rSAParameters.Exponent != null && rSAParameters.Exponent.Length == 3 && rSAParameters.Exponent[0] == 1 && rSAParameters.Exponent[1] == 0 && rSAParameters.Exponent[2] == 1 && (!requirePrivate || (rSAParameters.D != null && rSAParameters.D.Length != 0)); } catch { return false; } } private byte[] BuildPolicy(SentinelAdminDocument draft, long sequence, long issued, string callerKey) { if (!SentinelPolicy.CanonicalAtom(draft.Profile, 1, 64)) { throw new InvalidDataException("profile-invalid"); } if (!long.TryParse(draft.ExpiresUnixSeconds, NumberStyles.None, CultureInfo.InvariantCulture, out var result) || result < 0 || (result != 0L && result <= issued)) { throw new InvalidDataException("expiration-must-be-zero-or-future-unix-time"); } if (draft.UnknownMods != "Forbidden" && draft.UnknownMods != "Quarantined" && draft.UnknownMods != "Unmanaged") { throw new InvalidDataException("unknown-mod-policy-invalid"); } SortedDictionary<string, Rule> sortedDictionary = new SortedDictionary<string, Rule>(StringComparer.Ordinal); AddRules(draft.RequiredMods, "Required", sortedDictionary); AddRules(draft.OptionalMods, "ApprovedOptional", sortedDictionary); AddRules(draft.GrayListMods, "Unmanaged", sortedDictionary); AddRules(draft.ForbiddenMods, "Forbidden", sortedDictionary); SortedDictionary<string, SentinelAdministratorRole> sortedDictionary2 = ParseIdentities(draft.Administrators, "administrators"); SortedDictionary<string, SentinelAdministratorRole> sortedDictionary3 = ParseIdentities(draft.BannedUsers, "banned-users"); if (sortedDictionary2.Keys.Any(sortedDictionary3.ContainsKey)) { throw new InvalidDataException("identity-cannot-be-admin-and-banned"); } if (sortedDictionary2.Count == 0) { throw new InvalidDataException("at-least-one-administrator-required"); } if (callerKey != null && !sortedDictionary2.ContainsKey(callerKey) && sortedDictionary2.Count < 1) { throw new InvalidDataException("last-administrator-cannot-be-removed"); } IReadOnlyList<SentinelModuleRule> source = Array.Empty<SentinelModuleRule>(); StringBuilder stringBuilder = new StringBuilder(4096); stringBuilder.Append("RUNIC-SENTINEL/3\nprofile=").Append(draft.Profile).Append("\nsequence=") .Append(sequence.ToString(CultureInfo.InvariantCulture)) .Append("\nissued=") .Append(issued.ToString(CultureInfo.InvariantCulture)) .Append("\nexpires=") .Append(result.ToString(CultureInfo.InvariantCulture)) .Append("\nunknown=") .Append(draft.UnknownMods) .Append("\nunknown-capability=Forbidden\n"); foreach (Rule value in sortedDictionary.Values) { stringBuilder.Append("rule=").Append(value.Classification).Append('|') .Append(value.Id) .Append('|') .Append(value.Version) .Append('|') .Append(value.Hash) .Append('\n'); } foreach (SentinelModuleRule item in source.OrderBy<SentinelModuleRule, string>((SentinelModuleRule value) => value.Id, StringComparer.Ordinal)) { stringBuilder.Append("module=").Append(item.Scope).Append('|') .Append(item.Id) .Append('|') .Append(item.Version) .Append('|') .Append(item.Protocol.ToString(CultureInfo.InvariantCulture)) .Append('|') .Append(string.Join(",", item.Capabilities)) .Append('\n'); } foreach (SentinelAdministratorRole value2 in sortedDictionary2.Values) { stringBuilder.Append("role=").Append(value2.Authority).Append('|') .Append(Uri.EscapeDataString(value2.Subject)) .Append('\n'); } foreach (SentinelAdministratorRole value3 in sortedDictionary3.Values) { stringBuilder.Append("ban=").Append(value3.Authority).Append('|') .Append(Uri.EscapeDataString(value3.Subject)) .Append('\n'); } byte[] bytes = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true).GetBytes(stringBuilder.ToString()); if (bytes.Length > 1048576 || bytes.Length > 122880) { throw new InvalidDataException("policy-exceeds-admin-panel-bound"); } return bytes; } private SentinelAdminDocument DefaultDocument(AttestationSnapshot snapshot) { return new SentinelAdminDocument { Profile = "runic-suite", Sequence = 0L, ExpiresUnixSeconds = "0", UnknownMods = "Unmanaged", RequiredMods = string.Empty, Modules = string.Empty, AdmissionMode = (SentinelConfig.RemoteAdmissionPolicy?.Value ?? "Optional"), IntegritySeconds = "15", VeryHighThreshold = "2", HighThreshold = "3", EnforcementWindowSeconds = "60", BackupTransitions = true }; } private static void AddRules(string text, string classification, IDictionary<string, Rule> target) { foreach (string item in Lines(text)) { string[] array = item.Split('|'); if (array.Length != 3 || !SentinelPolicy.CanonicalPluginId(array[0]) || !SentinelPolicy.CanonicalVersionOrWildcard(array[1]) || !SentinelPolicy.CanonicalHashOrWildcard(array[2])) { throw new InvalidDataException("plugin-rule-invalid-" + item); } if (target.ContainsKey(array[0])) { throw new InvalidDataException("plugin-listed-more-than-once-" + array[0]); } target.Add(array[0], new Rule(classification, array[0], array[1], array[2])); } } private static SortedDictionary<string, SentinelAdministratorRole> ParseIdentities(string text, string label) { SortedDictionary<string, SentinelAdministratorRole> sortedDictionary = new SortedDictionary<string, SentinelAdministratorRole>(StringComparer.Ordinal); foreach (string item in Lines(text)) { string[] array = item.Split('|'); if (array.Length != 2 || !CanonicalAuthority(array[0]) || !CanonicalSubject(array[1])) { throw new InvalidDataException(label + "-identity-invalid-" + item); } string text2 = array[0] + ":" + Uri.EscapeDataString(array[1]); if (sortedDictionary.ContainsKey(text2)) { throw new InvalidDataException(label + "-duplicate-" + text2); } sortedDictionary.Add(text2, new SentinelAdministratorRole(array[0], array[1])); } return sortedDictionary; } private void ApplySettings(SentinelAdminDocument value) { int value2 = BoundedInt(value.IntegritySeconds, 5, 300, "integrity-seconds"); int value3 = BoundedInt(value.VeryHighThreshold, 1, 10, "very-high-threshold"); int value4 = BoundedInt(value.HighThreshold, 1, 20, "high-threshold"); int value5 = BoundedInt(value.EnforcementWindowSeconds, 10, 600, "enforcement-window"); if (value.AdmissionMode != "Disabled" && value.AdmissionMode != "Optional" && value.AdmissionMode != "Required") { throw new InvalidDataException("admission-mode-invalid"); } Set(SentinelConfig.IntegrityCheckSeconds, value2); Set(SentinelConfig.VeryHighDisconnectCount, value3); Set(SentinelConfig.HighDisconnectCount, value4); Set(SentinelConfig.EnforcementWindowSeconds, value5); Set(SentinelConfig.BackupBeforeTransitions, value.BackupTransitions); } private static void ValidateSettings(SentinelAdminDocument value) { BoundedInt(value.IntegritySeconds, 5, 300, "integrity-seconds"); BoundedInt(value.VeryHighThreshold, 1, 10, "very-high-threshold"); BoundedInt(value.HighThreshold, 1, 20, "high-threshold"); BoundedInt(value.EnforcementWindowSeconds, 10, 600, "enforcement-window"); if (value.AdmissionMode != "Disabled" && value.AdmissionMode != "Optional" && value.AdmissionMode != "Required") { throw new InvalidDataException("admission-mode-invalid"); } } private void ArchiveCurrent(long nextSequence) { string text = Path.Combine(_configRoot, "RunicSentinel", "policy-history", "before-sequence-" + nextSequence.ToString(CultureInfo.InvariantCulture)); Directory.CreateDirectory(text); CopyIfPresent(Resolve(SentinelConfig.PolicyFile?.Value), Path.Combine(text, "RunicSentinel.policy")); CopyIfPresent(Resolve(SentinelConfig.SignatureFile?.Value), Path.Combine(text, "RunicSentinel.policy.sig")); CopyIfPresent(Resolve(SentinelConfig.PublicKeyFile?.Value), Path.Combine(text, "RunicSentinel.policy.pub")); } private string Resolve(string configured) { if (!Path.IsPathRooted(configured ?? string.Empty)) { return Path.GetFullPath(Path.Combine(_configRoot, configured ?? string.Empty)); } return Path.GetFullPath(configured); } private static void AtomicWrite(string path, byte[] bytes) { Directory.CreateDirectory(Path.GetDirectoryName(path)); string text = path + ".admin.tmp"; using (FileStream fileStream = new FileStream(text, FileMode.Create, FileAccess.Write, FileShare.None, 65536, FileOptions.WriteThrough)) { fileStream.Write(bytes, 0, bytes.Length); fileStream.Flush(flushToDisk: true); } if (File.Exists(path)) { File.Replace(text, path, null); } else { File.Move(text, path); } } private static void WriteExclusive(string path, byte[] bytes) { using FileStream fileStream = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough); fileStream.Write(bytes, 0, bytes.Length); fileStream.Flush(flushToDisk: true); } private static byte[] EncodePrivate(RSAParameters value) { return Encoding.ASCII.GetBytes("RUNIC-RSA-PRIVATE/1\n" + Text("modulus", value.Modulus) + Text("exponent", value.Exponent) + Text("d", value.D) + Text("p", value.P) + Text("q", value.Q) + Text("dp", value.DP) + Text("dq", value.DQ) + Text("inverseq", value.InverseQ)); static string Text(string name, byte[] bytes) { return name + "=" + Convert.ToBase64String(bytes) + "\n"; } } private static RSAParameters DecodePrivate(byte[] bytes) { if (bytes == null || bytes.Length == 0 || bytes.Length > 65536) { throw new InvalidDataException("managed-key-size-invalid"); } string[] array = Encoding.ASCII.GetString(bytes).Split('\n'); if (array.Length != 10 || array[0] != "RUNIC-RSA-PRIVATE/1" || array[9].Length != 0) { throw new InvalidDataException("managed-key-format-invalid"); } Dictionary<string, byte[]> values = new Dictionary<string, byte[]>(StringComparer.Ordinal); for (int i = 1; i < 9; i++) { int num = array[i].IndexOf('='); if (num <= 0 || values.ContainsKey(array[i].Substring(0, num))) { throw new InvalidDataException("managed-key-field-invalid"); } try { values.Add(array[i].Substring(0, num), Convert.FromBase64String(array[i].Substring(num + 1))); } catch { throw new InvalidDataException("managed-key-base64-invalid"); } } return new RSAParameters { Modulus = Get("modulus"), Exponent = Get("exponent"), D = Get("d"), P = Get("p"), Q = Get("q"), DP = Get("dp"), DQ = Get("dq"), InverseQ = Get("inverseq") }; byte[] Get(string key) { if (!values.TryGetValue(key, out var value) || value.Length == 0) { throw new InvalidDataException("managed-key-field-missing-" + key); } return value; } } private static byte[] EncodePublic(RSAParameters value) { return Encoding.ASCII.GetBytes("RUNIC-RSA-PUBLIC/1\nmodulus=" + Convert.ToBase64String(value.Modulus) + "\nexponent=" + Convert.ToBase64String(value.Exponent) + "\n"); } private static string Sha256(byte[] bytes) { using SHA256 sHA = SHA256.Create(); return SentinelPolicy.Hex(sHA.ComputeHash(bytes)); } private static string PluginLines(SentinelPolicy policy, PluginClassification kind) { return string.Join("\n", from value in policy.Rules where value.Classification == kind select value.Id + "|" + value.Version + "|" + value.Sha256); } private static string IdentityLines(IEnumerable<SentinelAdministratorRole> values) { return string.Join("\n", from value in values.OrderBy<SentinelAdministratorRole, string>((SentinelAdministratorRole value) => value.CanonicalKey, StringComparer.Ordinal) select value.Authority + "|" + value.Subject); } private static string ModuleLines(IEnumerable<SentinelModuleRule> values) { return string.Join("\n", from value in values.OrderBy<SentinelModuleRule, string>((SentinelModuleRule value) => value.Id, StringComparer.Ordinal) select value.Scope.ToString() + "|" + value.Id + "|" + value.Version + "|" + value.Protocol + "|" + string.Join(",", value.Capabilities)); } private static IEnumerable<string> Lines(string value) { return from line in (value ?? string.Empty).Replace("\r", string.Empty).Split('\n') select line.Trim() into line where line.Length > 0 select line; } private static bool CanonicalAuthority(string value) { if (SentinelPolicy.CanonicalAtom(value, 1, 64)) { return value.All((char character) => character < 'A' || character > 'Z'); } return false; } private static bool CanonicalSubject(string value) { if (value != null && value.Length > 0 && value.Length <= 256 && !value.Any(char.IsControl) && !char.IsWhiteSpace(value[0])) { return !char.IsWhiteSpace(value[value.Length - 1]); } return false; } private static int BoundedInt(string value, int minimum, int maximum, string label) { if (!int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out var result) || result < minimum || result > maximum) { throw new InvalidDataException(label + "-invalid"); } return result; } private static void Set<T>(ConfigEntry<T> entry, T value) { if (entry != null && !EqualityComparer<T>.Default.Equals(entry.Value, value)) { entry.Value = value; } } private static void CopyIfPresent(string source, string destination) { if (File.Exists(source) && !File.Exists(destination)) { File.Copy(source, destination, overwrite: false); } } } internal static class SentinelNetworkMapWriter { private const int MaximumZdos = 16384; private const int MaximumEdges = 2048; private static readonly FieldInfo ObjectsField = AccessTools.Field(typeof(ZDOMan), "m_objectsByID"); private static readonly string[] ProductionRoles = new string[4] { "input", "fuel", "output", "replenishment" }; internal static bool TryAppend(StringBuilder builder, out string failure) { //IL_0108: Unknown result type (might be due to invalid IL or missing references) //IL_019d: Unknown result type (might be due to invalid IL or missing references) //IL_022b: Unknown result type (might be due to invalid IL or missing references) //IL_02a2: Unknown result type (might be due to invalid IL or missing references) failure = string.Empty; if (builder == null) { failure = "builder-missing"; return false; } ZNet instance = ZNet.instance; ZDOMan instance2 = ZDOMan.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer() || instance2 == null) { failure = "server-console-required"; return false; } if (!(ObjectsField?.GetValue(instance2) is Dictionary<ZDOID, ZDO> dictionary)) { failure = "world-index-unavailable"; return false; } int val = 0; int num = 0; int num2 = 0; builder.Append("network-map=server-local-snapshot\n"); foreach (KeyValuePair<ZDOID, ZDO> item in dictionary) { if (val++ >= 16384) { break; } ZDO value = item.Value; if (value == null || !value.IsValid()) { continue; } string value2 = value.GetString("runic.portals.record", string.Empty); string value3 = Safe(value.GetString("runic.portals.network", string.Empty), 64); if (!string.IsNullOrEmpty(value2) || !string.IsNullOrEmpty(value3)) { num++; builder.Append("portal=").Append(Id(item.Key)).Append('|') .Append(value3)