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); } }