Code/Endpoints/NetworkStorageRequestSecurity.cs
using System;
using System.Globalization;
using System.Text;

namespace Sandbox;

/// <summary>
/// Frontend helpers for request security metadata used by encrypted Network Storage calls.
/// </summary>
public static partial class NetworkStorage
{
	private const int MinimumEncryptedRequestRandomLength = 6;
	private const int DefaultEncryptedRequestRandomLength = 32;
	private static long _encryptedRequestSequence;

	/// <summary>
	/// Creates a one-use encrypted request id in "{unixSeconds}_{random}" format.
	/// The backend must reject stale or repeated ids before running endpoint logic.
	/// </summary>
	public static string CreateEncryptedRequestId()
	{
		return CreateEncryptedRequestId( DateTimeOffset.UtcNow, DefaultEncryptedRequestRandomLength );
	}

	/// <summary>
	/// Validates and parses an encrypted request id in "{unixSeconds}_{random6plus}" format.
	/// </summary>
	public static bool TryParseEncryptedRequestId( string requestId, out long unixSeconds, out string random )
	{
		unixSeconds = 0;
		random = null;

		if ( string.IsNullOrWhiteSpace( requestId ) )
			return false;

		var separatorIndex = requestId.IndexOf( '_' );
		if ( separatorIndex <= 0 || separatorIndex != requestId.LastIndexOf( '_' ) || separatorIndex == requestId.Length - 1 )
			return false;

		var unixPart = requestId[..separatorIndex];
		var randomPart = requestId[(separatorIndex + 1)..];
		if ( randomPart.Length < MinimumEncryptedRequestRandomLength )
			return false;

		foreach ( var ch in unixPart )
		{
			if ( ch < '0' || ch > '9' )
				return false;
		}

		foreach ( var ch in randomPart )
		{
			if ( !IsAsciiAlphaNumeric( ch ) )
				return false;
		}

		if ( !long.TryParse( unixPart, NumberStyles.None, CultureInfo.InvariantCulture, out unixSeconds ) || unixSeconds <= 0 )
			return false;

		random = randomPart;
		return true;
	}

	internal static string CreateEncryptedRequestId( DateTimeOffset now, int randomLength )
	{
		if ( randomLength < MinimumEncryptedRequestRandomLength )
			throw new ArgumentOutOfRangeException( nameof( randomLength ), $"Encrypted request ids need at least {MinimumEncryptedRequestRandomLength} random characters." );

		_encryptedRequestSequence++;
		return $"{now.ToUnixTimeSeconds()}_{CreateRandomAlphaNumeric( randomLength )}{_encryptedRequestSequence:x}{now.ToUnixTimeMilliseconds() % 1000:000}";
	}

	private static string CreateRandomAlphaNumeric( int length )
	{
		var sb = new StringBuilder( length );
		while ( sb.Length < length )
			sb.Append( Guid.NewGuid().ToString( "N" ) );

		return sb.ToString( 0, length );
	}

	private static bool IsAsciiAlphaNumeric( char ch )
	{
		return (ch >= '0' && ch <= '9')
			|| (ch >= 'A' && ch <= 'Z')
			|| (ch >= 'a' && ch <= 'z');
	}
}