Manages runtime security configuration for networking, including authentication session availability and request encryption requirements, based on a cryptographic pinning mechanism and server hints.
using System;
using System.Text.Json;
using System.Threading.Tasks;
namespace Sandbox;
public static partial class NetworkStorage
{
private static readonly TimeSpan RuntimeSecurityConfigMaxAge = TimeSpan.FromMinutes( 5 );
private static RuntimeSecurityConfigSnapshot _runtimeSecurityConfig;
private static Task<bool> _runtimeSecurityConfigLoadTask;
private static bool _securityConfigUnavailableLogged;
private static DateTimeOffset _securityConfigRetryAfter;
// Bumped when the pin, API root or project changes, so a fetch started before the change cannot commit its result.
private static int _securityConfigGeneration;
// Lowercase hex SHA-256 of the security-config signing key's SubjectPublicKeyInfo. Empty trusts the first key seen
// for each API root instead.
public static string SecuritySigningKeyPin { get; private set; } = "";
public static bool RuntimeSecurityConfigLoaded => _runtimeSecurityConfig?.Loaded == true;
public static bool RuntimeEnableAuthSessions => HasCapability( "auth-sessions" ) && (_runtimeSecurityConfig?.EnableAuthSessions ?? EnableAuthSessions);
public static bool RuntimeEnableEncryptedRequests => _runtimeSecurityConfig?.EnableEncryptedRequests ?? EnableEncryptedRequests;
public static string RuntimeSecurityConfigVersion => _runtimeSecurityConfig?.ConfigVersion ?? "";
public static string RuntimeSecurityClientMode => RuntimeEnableAuthSessions
? RuntimeEnableEncryptedRequests ? "session+encrypted" : "session"
: RuntimeEnableEncryptedRequests ? "encrypted" : "legacy";
public static string RuntimeSecurityConfigSource => _runtimeSecurityConfig?.Source ?? "not-loaded";
public static string LastSecurityConfigDiagnostics => BuildSecurityConfigDiagnostics();
// Read-only. Returns true with the stable error code and remediation text when the held security config was refused,
// which blocks endpoint calls. It is only reported for the project it was refused for, and it never refreshes,
// accepts or resets a config or exposes key material.
public static bool TryGetSecurityConfigError( out string code, out string message )
{
code = "";
message = "";
var runtime = _runtimeSecurityConfig;
if ( runtime is null || runtime.Trusted || string.IsNullOrEmpty( runtime.RefusalCode ) || runtime.ProjectId != ProjectId )
return false;
code = runtime.RefusalCode;
message = runtime.RefusalMessage;
return true;
}
public static bool IsSecurityConfigMismatchCode( string code )
=> !string.IsNullOrWhiteSpace( code )
&& code.StartsWith( "SECURITY_", StringComparison.OrdinalIgnoreCase );
public static async Task<bool> EnsureRuntimeSecurityConfigAsync( string reason = "first-use", bool forceRefresh = false )
{
EnsureConfigured();
if ( !forceRefresh && _runtimeSecurityConfig?.IsFresh == true )
return true;
if ( !forceRefresh && _securityConfigRetryAfter > DateTimeOffset.UtcNow )
return false;
if ( !forceRefresh && _runtimeSecurityConfigLoadTask is not null && !_runtimeSecurityConfigLoadTask.IsCompleted )
return await _runtimeSecurityConfigLoadTask;
_runtimeSecurityConfigLoadTask = LoadRuntimeSecurityConfigAsync( reason, forceRefresh );
return await _runtimeSecurityConfigLoadTask;
}
// Drops the held snapshot when the pin, API root or project changes. The next call verifies against the new settings,
// and a pinned game refuses encrypted calls until a config signed by the pinned key loads.
private static void ResetRuntimeSecurityConfig()
{
_securityConfigGeneration++;
_runtimeSecurityConfig = null;
_runtimeSecurityConfigLoadTask = null;
_securityConfigRetryAfter = default;
}
public static void MarkRuntimeSecurityMismatch( string code, string requestMode = "" )
{
if ( _runtimeSecurityConfig is null )
_runtimeSecurityConfig = RuntimeSecurityConfigSnapshot.FromSyncedSettings();
_runtimeSecurityConfig.LastMismatchCode = code ?? "";
_runtimeSecurityConfig.LastRequestMode = requestMode ?? "";
Log.Warning( $"[NetworkStorage] security mismatch code={code} mode={requestMode} diagnostics={LastSecurityConfigDiagnostics}" );
}
public static void ApplyServerExpectedSecurityConfig( string code, JsonElement expected, string expectedConfigVersion = null )
{
if ( expected.ValueKind != JsonValueKind.Object )
{
Log.Warning( $"[NetworkStorage] security mismatch {code} — no expected config provided, forcing refresh" );
_ = EnsureRuntimeSecurityConfigAsync( $"mismatch-{code}", forceRefresh: true );
return;
}
// A hint is not authority: once a signed config is trusted, pinned, or refused, only that config changes the mode.
if ( SecuritySigningKeyPin.Length > 0 || _runtimeSecurityConfig?.Trusted == true || !string.IsNullOrEmpty( _runtimeSecurityConfig?.RefusalCode ) )
{
Log.Warning( $"[NetworkStorage] ignoring expected config from {code}; signed config is authoritative" );
return;
}
var authSessions = expected.TryGetProperty( "authSessions", out var authProp )
? string.Equals( authProp.GetString(), "enabled", StringComparison.OrdinalIgnoreCase )
: RuntimeEnableAuthSessions;
var encryptedRequests = expected.TryGetProperty( "encryptedRequests", out var encProp )
? string.Equals( encProp.GetString(), "required", StringComparison.OrdinalIgnoreCase )
: RuntimeEnableEncryptedRequests;
// A hint may not switch encryption off; a MITM could otherwise strip it by forging an expected-config body.
encryptedRequests |= RuntimeEnableEncryptedRequests;
var previousMode = _runtimeSecurityConfig?.ModeText ?? "none";
var previousAuthSessions = RuntimeEnableAuthSessions;
var previousEncryptedRequests = RuntimeEnableEncryptedRequests;
var previousVersion = _runtimeSecurityConfig?.ConfigVersion ?? "";
var newVersion = !string.IsNullOrEmpty( expectedConfigVersion ) ? expectedConfigVersion : previousVersion;
_runtimeSecurityConfig = new RuntimeSecurityConfigSnapshot
{
Loaded = true,
Source = "server-expected",
ProjectId = ProjectId,
ConfigVersion = newVersion,
LoadedAt = DateTimeOffset.UtcNow,
TtlSeconds = 60,
EnableAuthSessions = authSessions,
EnableEncryptedRequests = encryptedRequests,
SyncedEnableAuthSessions = EnableAuthSessions,
SyncedEnableEncryptedRequests = EnableEncryptedRequests,
LastRefreshReason = code,
LastMismatchCode = code
};
EnableAuthSessions = authSessions;
EnableEncryptedRequests = encryptedRequests;
Log.Info( $"[NetworkStorage] security config auto-adapted from server expected values: "
+ $"authSessions={previousAuthSessions}->{authSessions} encryptedRequests={previousEncryptedRequests}->{encryptedRequests} "
+ $"version={previousVersion}->{newVersion} mode={previousMode}->{_runtimeSecurityConfig.ModeText} trigger={code}" );
}
}