From 6e6c8ee779e514fe313de4e5a7cec8eb25beb78e Mon Sep 17 00:00:00 2001 From: gamer147 Date: Thu, 28 May 2026 13:34:05 -0400 Subject: [PATCH] =?UTF-8?q?fix(signup):=20prestore=20client=20SID=E2=86=92?= =?UTF-8?q?UDID=20mapping=20so=20game=5Fstart=20can=20decrypt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After /tool/signup the client switches to SID-only headers (no UDID), so the next request's body can't be decrypted unless the server already knows the SID's UDID. ShadowverseSessionService now mirrors the client's Cute/Cryptographer.MakeMd5(viewerId + udid) formula (salt "r!I@ws8e5i="), and ToolController.Signup prestores the mapping at the end. Verified against a live signup capture: viewerId=1 + udid=62747917-93bc-454c-abb4-ef423b3c9317 produces the captured SID dc4aac79d35fe15dfb6262e0071bb03c. Note: this only fixes the fresh-signup path. Clients restarting with a cached viewer_id (which skip /tool/signup entirely) still hit the same issue — separate follow-up. Co-Authored-By: Claude Opus 4.7 --- .../Controllers/ToolController.cs | 15 +++++++- .../Services/ShadowverseSessionService.cs | 36 ++++++++++++++++++ .../ShadowverseSessionServiceTests.cs | 38 +++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 SVSim.UnitTests/Services/ShadowverseSessionServiceTests.cs diff --git a/SVSim.EmulatedEntrypoint/Controllers/ToolController.cs b/SVSim.EmulatedEntrypoint/Controllers/ToolController.cs index ea54700..6c67755 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/ToolController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/ToolController.cs @@ -4,6 +4,7 @@ using SVSim.Database.Repositories.Viewer; using SVSim.EmulatedEntrypoint.Extensions; using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; using SVSim.EmulatedEntrypoint.Models.Dtos.Responses; +using SVSim.EmulatedEntrypoint.Services; namespace SVSim.EmulatedEntrypoint.Controllers; @@ -11,11 +12,16 @@ public class ToolController : SVSimController { private readonly ILogger _logger; private readonly IViewerRepository _viewerRepository; + private readonly ShadowverseSessionService _sessionService; - public ToolController(ILogger logger, IViewerRepository viewerRepository) + public ToolController( + ILogger logger, + IViewerRepository viewerRepository, + ShadowverseSessionService sessionService) { _logger = logger; _viewerRepository = viewerRepository; + _sessionService = sessionService; } /// @@ -43,6 +49,13 @@ public class ToolController : SVSimController ?? await _viewerRepository.RegisterAnonymousViewer(udid); HttpContext.SetViewer(viewer); + + // Pre-store the SID the client will compute and use for its very next request. After + // signup the client switches to SID-only headers (no UDID), so without this mapping the + // translation middleware can't decrypt the next body. Formula mirrors the decompiled + // Cute/Certification.SessionId getter — see ShadowverseSessionService.ComputeClientSessionId. + _sessionService.StoreSessionForViewer(viewer.Id, udid); + _logger.LogInformation("Signup resolved for udid={Udid} → viewer_id={ViewerId}, short_udid={ShortUdid}.", udid, viewer.Id, viewer.ShortUdid); diff --git a/SVSim.EmulatedEntrypoint/Services/ShadowverseSessionService.cs b/SVSim.EmulatedEntrypoint/Services/ShadowverseSessionService.cs index 54a883b..0ed8612 100644 --- a/SVSim.EmulatedEntrypoint/Services/ShadowverseSessionService.cs +++ b/SVSim.EmulatedEntrypoint/Services/ShadowverseSessionService.cs @@ -1,9 +1,19 @@ 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() @@ -25,4 +35,30 @@ public class ShadowverseSessionService { _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); + } } \ No newline at end of file diff --git a/SVSim.UnitTests/Services/ShadowverseSessionServiceTests.cs b/SVSim.UnitTests/Services/ShadowverseSessionServiceTests.cs new file mode 100644 index 0000000..4f6e511 --- /dev/null +++ b/SVSim.UnitTests/Services/ShadowverseSessionServiceTests.cs @@ -0,0 +1,38 @@ +using NUnit.Framework; +using SVSim.EmulatedEntrypoint.Services; + +namespace SVSim.UnitTests.Services; + +public class ShadowverseSessionServiceTests +{ + /// + /// Fixture captured live from a fresh signup against this server. The client computed this + /// exact SID locally and sent it on the next /check/game_start request. Pinning the formula + /// here means any future refactor of + /// that drifts from Cute/Cryptographer.MakeMd5(viewerId + udid) will fail this test + /// before the user discovers it as a decrypt failure on game_start. + /// + [Test] + public void ComputeClientSessionId_matches_captured_fixture() + { + var svc = new ShadowverseSessionService(); + const long viewerId = 1; + var udid = new System.Guid("62747917-93bc-454c-abb4-ef423b3c9317"); + + string sid = svc.ComputeClientSessionId(viewerId, udid); + + Assert.That(sid, Is.EqualTo("dc4aac79d35fe15dfb6262e0071bb03c")); + } + + [Test] + public void StoreSessionForViewer_makes_sid_resolvable_to_udid() + { + var svc = new ShadowverseSessionService(); + const long viewerId = 1; + var udid = new System.Guid("62747917-93bc-454c-abb4-ef423b3c9317"); + + svc.StoreSessionForViewer(viewerId, udid); + + Assert.That(svc.GetUdidFromSessionId("dc4aac79d35fe15dfb6262e0071bb03c"), Is.EqualTo(udid)); + } +}