using System.Collections.Concurrent;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
namespace SVSim.EmulatedEntrypoint.Services;
public class ShadowverseSessionService
{
///
/// Salt the client's Cute/Cryptographer.MakeMd5 appends to every input before hashing.
/// Must match the decompiled client exactly — the server computes SIDs that the client
/// also computes locally for its outgoing request headers, and any mismatch breaks decrypt.
///
private const string MakeMd5Salt = "r!I@ws8e5i=";
private readonly ConcurrentDictionary _sessionIdToUdid;
public ShadowverseSessionService()
{
_sessionIdToUdid = new();
}
public Guid? GetUdidFromSessionId(string sid)
{
if (_sessionIdToUdid.TryGetValue(sid, out var udid))
{
return udid;
}
return null;
}
public void StoreUdidForSessionId(string sid, Guid udid)
{
_sessionIdToUdid.AddOrUpdate(sid, _ => udid, (_, _) => udid);
}
///
/// Replicates the client's Cute/Certification.SessionId getter:
/// MakeMd5(viewerId.ToString() + udid.ToString("D")). Returned as lowercase hex.
/// The client computes this once after signup and sends it as the SID header on every
/// subsequent request — the server must produce the same value to map back to the UDID.
///
public string ComputeClientSessionId(long viewerId, Guid udid)
{
string input = viewerId.ToString(CultureInfo.InvariantCulture)
+ udid.ToString("D")
+ MakeMd5Salt;
byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(hash).ToLowerInvariant();
}
///
/// Pre-stores the SID→UDID mapping the client will use for its first SID-only request
/// after /tool/signup. Without this, the translation middleware can't decrypt the
/// next request body (no UDID header, no mapping, falls back to Guid.Empty).
///
public void StoreSessionForViewer(long viewerId, Guid udid)
{
string sid = ComputeClientSessionId(viewerId, udid);
StoreUdidForSessionId(sid, udid);
}
}