Endpoints/NetworkStorageEncryptedEnvelope.cs
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( '"' );
}
}