fix(signup): prestore client SID→UDID mapping so game_start can decrypt
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<ToolController> _logger;
|
||||
private readonly IViewerRepository _viewerRepository;
|
||||
private readonly ShadowverseSessionService _sessionService;
|
||||
|
||||
public ToolController(ILogger<ToolController> logger, IViewerRepository viewerRepository)
|
||||
public ToolController(
|
||||
ILogger<ToolController> logger,
|
||||
IViewerRepository viewerRepository,
|
||||
ShadowverseSessionService sessionService)
|
||||
{
|
||||
_logger = logger;
|
||||
_viewerRepository = viewerRepository;
|
||||
_sessionService = sessionService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Salt the client's <c>Cute/Cryptographer.MakeMd5</c> 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.
|
||||
/// </summary>
|
||||
private const string MakeMd5Salt = "r!I@ws8e5i=";
|
||||
|
||||
private readonly ConcurrentDictionary<string, Guid> _sessionIdToUdid;
|
||||
|
||||
public ShadowverseSessionService()
|
||||
@@ -25,4 +35,30 @@ public class ShadowverseSessionService
|
||||
{
|
||||
_sessionIdToUdid.AddOrUpdate(sid, _ => udid, (_, _) => udid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replicates the client's <c>Cute/Certification.SessionId</c> getter:
|
||||
/// <c>MakeMd5(viewerId.ToString() + udid.ToString("D"))</c>. 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.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pre-stores the SID→UDID mapping the client will use for its first SID-only request
|
||||
/// after <c>/tool/signup</c>. Without this, the translation middleware can't decrypt the
|
||||
/// next request body (no UDID header, no mapping, falls back to <c>Guid.Empty</c>).
|
||||
/// </summary>
|
||||
public void StoreSessionForViewer(long viewerId, Guid udid)
|
||||
{
|
||||
string sid = ComputeClientSessionId(viewerId, udid);
|
||||
StoreUdidForSessionId(sid, udid);
|
||||
}
|
||||
}
|
||||
38
SVSim.UnitTests/Services/ShadowverseSessionServiceTests.cs
Normal file
38
SVSim.UnitTests/Services/ShadowverseSessionServiceTests.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using NUnit.Framework;
|
||||
using SVSim.EmulatedEntrypoint.Services;
|
||||
|
||||
namespace SVSim.UnitTests.Services;
|
||||
|
||||
public class ShadowverseSessionServiceTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <see cref="ShadowverseSessionService.ComputeClientSessionId"/>
|
||||
/// that drifts from <c>Cute/Cryptographer.MakeMd5(viewerId + udid)</c> will fail this test
|
||||
/// before the user discovers it as a decrypt failure on game_start.
|
||||
/// </summary>
|
||||
[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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user