Endpoints/NetworkStorageEncryptedEnvelope.cs

Helper functions for creating an encrypted request envelope for network storage. It builds an envelope dictionary with metadata, derives an encryption key from the ApiKey and envelope context, encrypts payload with AES-256-GCM, computes HMAC-SHA256 signature, and provides stable JSON serialization helpers.

Native InteropEmbedded Secret
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;

namespace Sandbox;

public static partial class NetworkStorage
{
	private static Dictionary<string, object> CreateEncryptedEndpointEnvelope( string slug, Dictionary<string, object> payload, RuntimeAuthSession session )
	{
		var envelope = new Dictionary<string, object>
		{
			["version"] = "1",
			["algorithm"] = "aes-256-gcm+hmac-sha256",
			["projectId"] = ProjectId,
			["nonce"] = Base64UrlEncode( Encoding.UTF8.GetBytes( Guid.NewGuid().ToString( "N" ) ) ),
			["publicKeyFingerprint"] = PublicKeyFingerprint( ApiKey )
		};
		if ( session is not null )
			envelope["sessionId"] = session.Id;
		else
			envelope["steamId"] = Game.SteamId.ToString();

		var iv = Guid.NewGuid().ToByteArray()[..12];
		envelope["iv"] = Base64UrlEncode( iv );
		var key = Sha256( Encoding.UTF8.GetBytes( $"sboxcool.network-storage.encrypted-request.v1\0{ApiKey ?? ""}\0{StableStringify( EnvelopeContext( envelope ) )}" ) );
		var plaintext = Encoding.UTF8.GetBytes( JsonSerializer.Serialize( payload ) );
		var aad = Encoding.UTF8.GetBytes( StableStringify( EnvelopeContext( envelope ) ) );
		var (ciphertext, tag) = Aes256GcmEncrypt( key, iv, plaintext, aad );

		envelope["tag"] = Base64UrlEncode( tag );
		envelope["encryptedPayload"] = Base64UrlEncode( ciphertext );
		envelope["signature"] = ToHex( HmacSha256( Encoding.UTF8.GetBytes( ApiKey ?? "" ), Encoding.UTF8.GetBytes( SignatureBinding( envelope ) ) ) );
		return envelope;
	}

	private static Dictionary<string, object> EnvelopeContext( Dictionary<string, object> envelope )
	{
		var identity = envelope.ContainsKey( "sessionId" )
			? ("session", Convert.ToString( envelope["sessionId"] ) ?? "")
			: ("steam", Convert.ToString( envelope["steamId"] ) ?? "");
		return new Dictionary<string, object>
		{
			["version"] = Convert.ToString( envelope["version"] ) ?? "1",
			["algorithm"] = Convert.ToString( envelope["algorithm"] ) ?? "aes-256-gcm+hmac-sha256",
			["projectId"] = Convert.ToString( envelope["projectId"] ) ?? "",
			["identityType"] = identity.Item1,
			["identity"] = identity.Item2,
			["nonce"] = Convert.ToString( envelope["nonce"] ) ?? "",
			["publicKeyFingerprint"] = Convert.ToString( envelope["publicKeyFingerprint"] ) ?? ""
		};
	}

	private static string SignatureBinding( Dictionary<string, object> envelope )
	{
		var binding = EnvelopeContext( envelope );
		binding["iv"] = Convert.ToString( envelope["iv"] ) ?? "";
		binding["encryptedPayload"] = Convert.ToString( envelope["encryptedPayload"] ) ?? "";
		binding["tag"] = Convert.ToString( envelope["tag"] ) ?? "";
		return StableStringify( binding );
	}

	private static Dictionary<string, object> ObjectToDictionary( object input )
	{
		if ( input is Dictionary<string, object> existing )
			return new Dictionary<string, object>( existing );

		var json = JsonSerializer.Serialize( input ?? new { } );
		return JsonSerializer.Deserialize<Dictionary<string, object>>( json ) ?? new Dictionary<string, object>();
	}

	private static string PublicKeyFingerprint( string apiKey )
		=> ToHex( Sha256( Encoding.UTF8.GetBytes( apiKey ?? "" ) ) )[..32];

	private static string ReadServerMessage( JsonElement root, string fallback )
	{
		if ( root.TryGetProperty( "message", out var message ) && message.ValueKind == JsonValueKind.String )
			return message.GetString() ?? fallback;
		if ( root.TryGetProperty( "error", out var error ) && error.ValueKind == JsonValueKind.Object &&
			error.TryGetProperty( "message", out var nested ) && nested.ValueKind == JsonValueKind.String )
			return nested.GetString() ?? fallback;
		return fallback;
	}

	private static string StableStringify( object value )
	{
		var sb = new StringBuilder();
		WriteStableJson( sb, value );
		return sb.ToString();
	}

	private static void WriteStableJson( StringBuilder sb, object value )
	{
		if ( value is Dictionary<string, object> dict )
		{
			sb.Append( '{' );
			var keys = new List<string>( dict.Keys );
			keys.Sort( StringComparer.Ordinal );
			for ( var i = 0; i < keys.Count; i++ )
			{
				if ( i > 0 ) sb.Append( ',' );
				WriteJsonString( sb, keys[i] );
				sb.Append( ':' );
				WriteStableJson( sb, dict[keys[i]] );
			}
			sb.Append( '}' );
			return;
		}
		if ( value is string stringValue )
		{
			WriteJsonString( sb, stringValue );
			return;
		}
		sb.Append( JsonSerializer.Serialize( value ) );
	}

	private static void WriteJsonString( StringBuilder sb, string value )
	{
		sb.Append( '"' );
		foreach ( var ch in value ?? "" )
		{
			switch ( ch )
			{
				case '"': sb.Append( "\\\"" ); break;
				case '\\': sb.Append( "\\\\" ); break;
				case '\b': sb.Append( "\\b" ); break;
				case '\f': sb.Append( "\\f" ); break;
				case '\n': sb.Append( "\\n" ); break;
				case '\r': sb.Append( "\\r" ); break;
				case '\t': sb.Append( "\\t" ); break;
				default:
					if ( ch < ' ' )
						sb.Append( "\\u" ).Append( ((int)ch).ToString( "x4" ) );
					else
						sb.Append( ch );
					break;
			}
		}
		sb.Append( '"' );
	}

}