mirror of
https://github.com/djdevin/recnet-plugin.git
synced 2026-09-08 06:31:29 -07:00
Support for build 20230414 (#5)
* support for 20230331, remove need for metadata patching * build 20230414
This commit is contained in:
@@ -42,11 +42,88 @@ dotnet build -c Debug -p:GamePath="C:\Games\depots\471711\23191908"
|
||||
patch.
|
||||
|
||||
4. **Obfuscated members live in the global namespace** and are referenced unqualified in this codebase
|
||||
(e.g. `typeof(JAPJPGNBMNM)`, `PGECJHKNIEN`). No `using` needed.
|
||||
(e.g. `typeof(LEALBOODIEE)`, `PGECJHKNIEN`). No `using` needed.
|
||||
|
||||
5. **Harmony prefix conventions here:** force a value + `return false` to skip the original (see
|
||||
`Patches/EACPatches.cs`). For out-params, take a `ref` parameter named exactly as the interop shows
|
||||
it (e.g. `ref string ALOMDLLNIMD`), plus `ref bool __result`.
|
||||
`Patches/EACPatches.cs`). **Bind parameters positionally (`__0`, `__1`, …), never by their
|
||||
obfuscated name** — obfuscated *parameter* names change per build just like type names, and binding
|
||||
by name fails at load with `Parameter "XXX" not found in method ...`. For out-params take
|
||||
`ref string __0` plus `ref bool __result`.
|
||||
|
||||
6. **Method names in `[HarmonyPatch]` are strings — the compiler does not check them.** A renamed
|
||||
*type* is a build error; a renamed *method* builds fine and only shows up as a HarmonyX error in
|
||||
`LogOutput.log` at load, or (worse) as a patch that silently never runs. After a game upgrade,
|
||||
re-verify every string method name with Cecil, don't just trust a green build.
|
||||
|
||||
### Surviving a game-version upgrade
|
||||
|
||||
Obfuscated names are re-rolled every build. Unobfuscated names (`CheckForDUIDMismatch`, `WriteDUIDs`,
|
||||
`ClearDUIDs`, `SendRequest`, `NotifyServerCertificate`, `GenerateChallengeResponse`) have been stable
|
||||
across upgrades so far; everything else must be re-resolved. Don't guess from the old name — **search
|
||||
the new interop by signature**, which is what actually identifies the target:
|
||||
|
||||
| Patch | Target | Signature that identifies it |
|
||||
| --- | --- | --- |
|
||||
| `PhotonPatches` | `HPEENKELKDJ.MGKINLFMJLB` | only instance, 0-param method returning `Photon.Realtime.AppSettings` in `Assembly-CSharp` (the static/2-param `PUNNetworkManager` sibling also returns it — exclude it) |
|
||||
| `EACPatches` (is-ready) | `EACManager.MCFIOBHCFBB` | only static, 0-param `bool` on `EACManager` that isn't a property getter; type lives in `RecRoom.Rranticheat.Runtime.dll` |
|
||||
|
||||
> **Il2CppInterop regenerates on launch and obfuscation can differ between generations.** Only scan the
|
||||
> interop the game *actually loaded* — check that its mtime is *after* the last game launch, and treat a
|
||||
> clean HarmonyX load (no "Could not find method") as the real proof. A stale/mismatched interop
|
||||
> generation once produced a whole different name set (`IHODDIDPEOD.JEGOHKJDPFH`, `EACManager.KOIFGPGJGKB`)
|
||||
> that got overwritten on the next launch back to the names below — patching against it failed at load.
|
||||
|
||||
Renames observed in the **20230414 build** (`C:\Games\recflare-client`, Steam manifest
|
||||
`3668280474894052876`):
|
||||
|
||||
- `LEALBOODIEE.GBNKOFMAJPA` → `HPEENKELKDJ.MGKINLFMJLB`
|
||||
- `EACManager.IMMGELPFGCK` → `EACManager.MCFIOBHCFBB`
|
||||
- `CheckForDUIDMismatch` out-param → `BPOGCIINKBB` (still bound as `__0`, no source change)
|
||||
|
||||
Renames observed in the **07-21 build** (`C:\Games\recflare-client`), for reference:
|
||||
|
||||
- `CheckForDUIDMismatch` out-param `ALOMDLLNIMD` → `LICOPEEMHHG` (now bound as `__0`)
|
||||
- `GenerateChallengeResponse` param `PGCINMIEBJP` → `__0`
|
||||
- `GPFPFDBGCEK.AMOHMPKKGHL` → `LEALBOODIEE.GBNKOFMAJPA`
|
||||
- `EACManager.FJLMLEPOKGE` → `EACManager.IMMGELPFGCK`
|
||||
- `JAPJPGNBMNM`, `HPHDJAFFHCN<>`, `HAAHJPGNIMD` — **gone** from `Assembly-CSharp`. These were the
|
||||
image-signing `PromisePatch`, now deleted and replaced by `ImageSigningPatch` (see below), which
|
||||
hooks framework types instead and so has no obfuscated names left to break.
|
||||
|
||||
## Image signature verification
|
||||
|
||||
The client verifies images against an RSA public key whose modulus is a string literal in
|
||||
`global-metadata.dat`. Patching that literal is fragile; **don't**. Hook the framework instead.
|
||||
|
||||
The decisive observation: the literal base64-decodes to **exactly 256 bytes and does not start with
|
||||
`0x30`**, so it is a *raw* 2048-bit modulus, not a DER/SPKI blob. The client therefore cannot hand it
|
||||
to a key-import API — it has to base64-decode it and hand-build `RSAParameters { Modulus, Exponent }`.
|
||||
Those are plain `mscorlib` calls with unobfuscated names. Confirmed working at runtime: images load
|
||||
with no signing check.
|
||||
|
||||
`Patches/ImageSigningPatch.cs` hooks, all in `Il2Cppmscorlib.dll`:
|
||||
|
||||
| Hook | Purpose |
|
||||
| --- | --- |
|
||||
| `Convert.FromBase64String` | catches the literal regardless of which crypto stack consumes it (guarded by a length check first — it runs for every base64 decode in the game) |
|
||||
| `RSACryptoServiceProvider.ImportParameters` | swaps the modulus in place (both keys are 2048-bit, so no realloc) |
|
||||
| `RSACryptoServiceProvider.VerifyData` / `VerifyHash` ×2 | forces the verify to succeed |
|
||||
|
||||
Config lives in `[Signing]`. `Disable Signature Verification` defaults **true** — that's the shipped
|
||||
behaviour, since this setup doesn't use image signing. `Signing Modulus Override` is the secondary
|
||||
path for a deployment that *does* want signed images (keeps real verification, against your keypair).
|
||||
|
||||
Two things to remember:
|
||||
|
||||
- **Blast radius:** forcing verify-true affects *all* mscorlib RSA verification, not just images.
|
||||
BestHTTP's TLS uses its own bundled BouncyCastle, so cert validation appears unaffected — inferred
|
||||
from assembly layout, not proven. Keep it behind the config knob.
|
||||
- **If it ever stops working:** the patch logs `[SIG] stock modulus seen at <hook>` on first sighting.
|
||||
No such line = verification moved to `BestHTTP.SecureProtocol.Org.BouncyCastle`; the equivalent
|
||||
hooks there are `RsaKeyParameters..ctor(bool, BigInteger, BigInteger)` and the **concrete**
|
||||
`RsaDigestSigner`/`PssSigner.VerifySignature` (not the abstract `ISigner` — gotcha 3).
|
||||
- The stock modulus is hardcoded in the patch. If a future build re-rolls the key, the match silently
|
||||
stops firing; that log line is how you'd notice.
|
||||
|
||||
## Inspecting the game
|
||||
|
||||
@@ -55,13 +132,19 @@ names, which assembly a concrete impl lives in). Mark-of-the-web will block load
|
||||
directly — copy it somewhere local, `Unblock-File`, then load via bytes:
|
||||
|
||||
```powershell
|
||||
$interop = "$env:GamePath\BepInEx\interop"
|
||||
$dst = "$scratch\Mono.Cecil.dll"
|
||||
Copy-Item "$interop\Mono.Cecil.dll" $dst; Unblock-File $dst
|
||||
# NOTE: Mono.Cecil.dll ships in BepInEx/core, NOT in BepInEx/interop.
|
||||
Copy-Item "$env:GamePath\BepInEx\core\Mono.Cecil.dll" $dst; Unblock-File $dst
|
||||
[System.Reflection.Assembly]::Load([System.IO.File]::ReadAllBytes($dst)) | Out-Null
|
||||
$asm = [Mono.Cecil.AssemblyDefinition]::ReadAssembly("$interop\Assembly-CSharp.dll")
|
||||
# then walk $asm.MainModule.GetTypes(), inspect .Methods / .Fields / .IsAbstract / .IsStatic ...
|
||||
```
|
||||
|
||||
Not every target is in `Assembly-CSharp` — `EACManager` is in `RecRoom.Rranticheat.Runtime.dll`,
|
||||
`HTTPManager`/`LegacyTlsAuthentication` in `RecNet.Runtime.dll`. When a lookup comes up empty, sweep
|
||||
all ~305 DLLs in `interop/` (`ReadAssembly` each, `.Dispose()` after) before concluding it's gone.
|
||||
|
||||
Notes:
|
||||
- Windows PowerShell 5.1 has **no** `?.` null-conditional operator — use explicit `$x -eq $null` checks.
|
||||
- String literals (pref keys, endpoints, GUIDs) are in `RecRoom_Data/il2cpp_data/Metadata/global-metadata.dat`.
|
||||
|
||||
@@ -21,11 +21,13 @@ public static class DUIDMismatchPatch
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(CheatManager), "CheckForDUIDMismatch")]
|
||||
private static bool Prefix(ref string ALOMDLLNIMD, ref bool __result)
|
||||
// __0 = the out-param (positional). Its obfuscated name changes every game build, so binding it
|
||||
// by name throws "Parameter ... not found" on upgrade.
|
||||
private static bool Prefix(ref string __0, ref bool __result)
|
||||
{
|
||||
if (Plugin.SimulateDUIDMismatch.Value)
|
||||
{
|
||||
ALOMDLLNIMD = SimulatedStoredDeviceId;
|
||||
__0 = SimulatedStoredDeviceId;
|
||||
__result = true;
|
||||
Plugin.Log.LogWarning($"[DUID] simulating mismatch, stored id = {SimulatedStoredDeviceId}");
|
||||
return false;
|
||||
@@ -33,7 +35,7 @@ public static class DUIDMismatchPatch
|
||||
|
||||
if (Plugin.SuppressDUIDMismatch.Value)
|
||||
{
|
||||
ALOMDLLNIMD = string.Empty;
|
||||
__0 = string.Empty;
|
||||
__result = false;
|
||||
Plugin.Log.LogInfo("[DUID] mismatch check forced to false (suppressed)");
|
||||
return false;
|
||||
|
||||
@@ -9,7 +9,10 @@ namespace RecNetPlugin.Patches;
|
||||
public static class EACPatches
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(EACManager), "FJLMLEPOKGE")]
|
||||
// The "is ready" check: the only static, 0-param bool method on EACManager that isn't a property
|
||||
// getter. 20230414 build: MCFIOBHCFBB (was IMMGELPFGCK, was FJLMLEPOKGE). Method names here are
|
||||
// strings, so a rename is not a compile error — it shows up as a HarmonyX "method not found" at load.
|
||||
[HarmonyPatch(typeof(EACManager), "MCFIOBHCFBB")]
|
||||
private static bool IsReadyPatch(ref bool __result)
|
||||
{
|
||||
__result = true;
|
||||
@@ -18,10 +21,11 @@ public static class EACPatches
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(EACManager), "GenerateChallengeResponse")]
|
||||
private static bool GenerateChallengeResponsePatch(string PGCINMIEBJP, ref string __result)
|
||||
// __0 = the challenge string (positional); obfuscated param names shift between game builds.
|
||||
private static bool GenerateChallengeResponsePatch(string __0, ref string __result)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(PGCINMIEBJP))
|
||||
__result = Convert.ToBase64String(Encoding.UTF8.GetBytes(PGCINMIEBJP));
|
||||
if (!string.IsNullOrEmpty(__0))
|
||||
__result = Convert.ToBase64String(Encoding.UTF8.GetBytes(__0));
|
||||
else
|
||||
__result = Convert.ToBase64String(Encoding.UTF8.GetBytes("nothing"));
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
using System;
|
||||
using HarmonyLib;
|
||||
using Il2CppInterop.Runtime.InteropTypes.Arrays;
|
||||
using Il2CppSystem.Security.Cryptography;
|
||||
using Convert = Il2CppSystem.Convert;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
// Image signing: the client verifies images against an RSA public key whose modulus is a string
|
||||
// literal in global-metadata.dat. Patching that literal is fragile, so we intervene at the framework
|
||||
// level instead, between the literal and the verify.
|
||||
//
|
||||
// The stored value is 256 bytes once base64-decoded and does NOT start with 0x30, so it is a RAW
|
||||
// 2048-bit modulus, not a DER/SPKI blob. The client therefore has to base64-decode it and hand-build
|
||||
// an RSAParameters { Modulus, Exponent }. Both of those steps are plain framework calls with
|
||||
// unobfuscated names, which is why hooking here survives game rebuilds (unlike the obfuscated
|
||||
// PromisePatch this replaces).
|
||||
//
|
||||
// Two knobs, see [Signing] in the .cfg:
|
||||
// Disable Signature Verification -> THE FIX (default true). Forces the RSA verify to succeed, so
|
||||
// the modulus never has to match and unsigned images load. This
|
||||
// self-hosted setup does not use image signing.
|
||||
// Signing Modulus Override -> secondary: swap in your own modulus and keep real verification,
|
||||
// for a deployment that DOES want signed images. Ignored (well,
|
||||
// redundant) while the verify is disabled.
|
||||
//
|
||||
// Both are belt-and-braces: we swap at Convert.FromBase64String (catches the value regardless of
|
||||
// which crypto stack consumes it) AND at ImportParameters (catches it if the client uses some other
|
||||
// base64 decoder). Whichever fires first wins; the second sees the already-swapped value and no-ops.
|
||||
//
|
||||
// If the log never shows "[SIG] stock modulus seen", the client is not using mscorlib RSA at all —
|
||||
// the fallback is BestHTTP.SecureProtocol.Org.BouncyCastle, where the equivalent hooks are
|
||||
// RsaKeyParameters..ctor(bool, BigInteger, BigInteger) and the concrete RsaDigestSigner/PssSigner
|
||||
// .VerifySignature (NOT the ISigner "interface", which is abstract and never dispatches).
|
||||
[HarmonyPatch]
|
||||
public static class ImageSigningPatch
|
||||
{
|
||||
// The stock 2048-bit public modulus baked into global-metadata.dat (base64, 344 chars).
|
||||
private const string StockModulusBase64 =
|
||||
"X07yXkxaaLcZ1wVXfkWjgFkkqdoLhDFm0GPODsF+Q47pSUlbLvtXGqStnyEJEIrQmgDiicAvCdGRq4lovr2l5sIP" +
|
||||
"MaoyizsbVHBdwLUrCsji0RvSBnmvN+8KqQ8STnB4DP4pAsPilfD35def4WuX/xMCXB5+hQUVhv27HPV8Dj9XzHuJ" +
|
||||
"AijIM9UwDZmvUcECyiO4wv+TaZi2+ELBtaLCQR8Gm1ZPeDEwP62Ch6MJy0jx5pkvvD0KdF9Wye+3/Wx31Zn/Trdo" +
|
||||
"9HL4sGFWPDM9H9kQhZd5wkTHuxpwGIIhlzIwvY2/pBGdZKP6fi1D2jROEmVkBDyhmYY9nO+s3/bndQ==";
|
||||
|
||||
private const int ModulusBytes = 256;
|
||||
|
||||
private static byte[] _stockModulus;
|
||||
private static byte[] _override;
|
||||
private static bool _overrideResolved;
|
||||
private static bool _loggedSeen;
|
||||
private static bool _loggedForced;
|
||||
|
||||
private static byte[] Stock => _stockModulus ??= System.Convert.FromBase64String(StockModulusBase64);
|
||||
|
||||
// Decoded lazily and cached, so a malformed config value is reported once instead of per call.
|
||||
private static byte[] Override
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_overrideResolved)
|
||||
return _override;
|
||||
_overrideResolved = true;
|
||||
|
||||
var raw = Plugin.SigningModulusOverride.Value;
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
return _override = null;
|
||||
|
||||
try
|
||||
{
|
||||
var bytes = System.Convert.FromBase64String(raw.Trim());
|
||||
if (bytes.Length != ModulusBytes)
|
||||
{
|
||||
Plugin.Log.LogError(
|
||||
$"[SIG] 'Signing Modulus Override' decoded to {bytes.Length} bytes, expected {ModulusBytes} " +
|
||||
"(a raw 2048-bit modulus). Ignoring it. Note this must be the bare modulus, NOT a PEM/DER key.");
|
||||
return _override = null;
|
||||
}
|
||||
|
||||
Plugin.Log.LogWarning("[SIG] signing modulus override active");
|
||||
return _override = bytes;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
Plugin.Log.LogError("[SIG] 'Signing Modulus Override' is not valid base64. Ignoring it.");
|
||||
return _override = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsStock(Il2CppStructArray<byte> value)
|
||||
{
|
||||
if (value == null || value.Length != ModulusBytes)
|
||||
return false;
|
||||
|
||||
var stock = Stock;
|
||||
for (var i = 0; i < ModulusBytes; i++)
|
||||
if (value[i] != stock[i])
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void NoteSeen(string where)
|
||||
{
|
||||
if (_loggedSeen)
|
||||
return;
|
||||
_loggedSeen = true;
|
||||
Plugin.Log.LogWarning($"[SIG] stock modulus seen at {where}");
|
||||
}
|
||||
|
||||
// Choke point 1: the base64 decode of the literal. Guarded by a length check first, because this
|
||||
// runs for every base64 decode in the game.
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(Convert), nameof(Convert.FromBase64String))]
|
||||
private static bool FromBase64StringPrefix(string __0, ref Il2CppStructArray<byte> __result)
|
||||
{
|
||||
if (__0 == null || __0.Length != StockModulusBase64.Length || __0 != StockModulusBase64)
|
||||
return true;
|
||||
|
||||
NoteSeen("Convert.FromBase64String");
|
||||
|
||||
var replacement = Override;
|
||||
if (replacement == null)
|
||||
return true;
|
||||
|
||||
var arr = new Il2CppStructArray<byte>(ModulusBytes);
|
||||
for (var i = 0; i < ModulusBytes; i++)
|
||||
arr[i] = replacement[i];
|
||||
|
||||
__result = arr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Choke point 2: the key import. Mutates the modulus in place — both are 2048-bit, so the array
|
||||
// is already the right size and no reallocation is needed.
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(RSACryptoServiceProvider), nameof(RSACryptoServiceProvider.ImportParameters))]
|
||||
private static void ImportParametersPrefix(RSAParameters __0)
|
||||
{
|
||||
var modulus = __0?.Modulus;
|
||||
if (!IsStock(modulus))
|
||||
return;
|
||||
|
||||
NoteSeen("RSACryptoServiceProvider.ImportParameters");
|
||||
|
||||
var replacement = Override;
|
||||
if (replacement == null)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < ModulusBytes; i++)
|
||||
modulus[i] = replacement[i];
|
||||
}
|
||||
|
||||
// Forces EVERY mscorlib RSA verification to succeed, not just image signatures. BestHTTP's TLS
|
||||
// uses its own bundled BouncyCastle rather than mscorlib RSA, so this should not touch
|
||||
// certificate validation — but it is a blunt instrument, so it stays behind a config knob rather
|
||||
// than being unconditional.
|
||||
private static bool ForceVerifyTrue(ref bool __result)
|
||||
{
|
||||
if (!Plugin.DisableSignatureVerification.Value)
|
||||
return true;
|
||||
|
||||
if (!_loggedForced)
|
||||
{
|
||||
_loggedForced = true;
|
||||
Plugin.Log.LogWarning("[SIG] signature verification disabled — RSA verify forced to true");
|
||||
}
|
||||
|
||||
__result = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(RSACryptoServiceProvider), nameof(RSACryptoServiceProvider.VerifyData))]
|
||||
private static bool VerifyDataPrefix(ref bool __result) => ForceVerifyTrue(ref __result);
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(RSACryptoServiceProvider), nameof(RSACryptoServiceProvider.VerifyHash),
|
||||
[typeof(Il2CppStructArray<byte>), typeof(int), typeof(Il2CppStructArray<byte>)])]
|
||||
private static bool VerifyHashPrefix(ref bool __result) => ForceVerifyTrue(ref __result);
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(RSACryptoServiceProvider), nameof(RSACryptoServiceProvider.VerifyHash),
|
||||
[typeof(Il2CppStructArray<byte>), typeof(Il2CppStructArray<byte>), typeof(HashAlgorithmName),
|
||||
typeof(RSASignaturePadding)])]
|
||||
private static bool VerifyHashPaddingPrefix(ref bool __result) => ForceVerifyTrue(ref __result);
|
||||
}
|
||||
@@ -7,7 +7,10 @@ namespace RecNetPlugin.Patches;
|
||||
/**
|
||||
Patches Photon to use the App IDs and server hostname/port specified in the plugin config.
|
||||
*/
|
||||
[HarmonyPatch(typeof(GPFPFDBGCEK), "AMOHMPKKGHL")]
|
||||
// Obfuscated names shift every game build. Re-resolve by signature: the target is the only
|
||||
// instance, 0-param method returning Photon.Realtime.AppSettings in Assembly-CSharp.
|
||||
// 20230414 build: HPEENKELKDJ.MGKINLFMJLB (was LEALBOODIEE.GBNKOFMAJPA, was GPFPFDBGCEK.AMOHMPKKGHL).
|
||||
[HarmonyPatch(typeof(HPEENKELKDJ), "MGKINLFMJLB")]
|
||||
public class PhotonPatches
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
using HarmonyLib;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
/**
|
||||
* This allows the global-metadata.dat to be different on the client
|
||||
* patched to allow a different modulus so we can sign images.
|
||||
*/
|
||||
[HarmonyPatch(typeof(JAPJPGNBMNM), "JOKECJKBJGD")]
|
||||
public static class PromisePatch
|
||||
{
|
||||
public static bool Prefix(out HPHDJAFFHCN<JAPJPGNBMNM.AOFCCEACNNA> __result)
|
||||
{
|
||||
var result = JAPJPGNBMNM.AOFCCEACNNA.JGIHNLEFJEL();
|
||||
var promise = HAAHJPGNIMD.NMOOLKAJDOC(result);
|
||||
__result = promise;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@ public class Plugin : BasePlugin
|
||||
public static ConfigEntry<bool> RestoreStoredDUID { get; private set; }
|
||||
public static ConfigEntry<string> DeviceIdResponseOverride { get; private set; }
|
||||
public static ConfigEntry<int> DeviceIdResponseStatus { get; private set; }
|
||||
public static ConfigEntry<bool> DisableSignatureVerification { get; private set; }
|
||||
public static ConfigEntry<string> SigningModulusOverride { get; private set; }
|
||||
|
||||
private static bool _corruptDone;
|
||||
|
||||
@@ -50,6 +52,9 @@ public class Plugin : BasePlugin
|
||||
DeviceIdResponseOverride = Config.Bind("Advanced", "DeviceId Response Override", "", "Replace the body of the PlayerReporting/v1/deviceId response with this text, to test what shape the client will accept. Empty = leave the server's response alone.");
|
||||
DeviceIdResponseStatus = Config.Bind("Advanced", "DeviceId Response Status", 200, "HTTP status to force on the PlayerReporting/v1/deviceId response. Only applies when the override body is set.");
|
||||
|
||||
DisableSignatureVerification = Config.Bind("Signing", "Disable Signature Verification", true, "Force RSA signature verification to succeed (ON by default), so the client stops checking that images are signed with Rec Room's private key. This is what lets a self-hosted server serve its own images without the baked-in modulus matching. Set false only if you actually want signed images, in which case use 'Signing Modulus Override' instead. NOTE: this forces ALL mscorlib RSA verification to pass, not just image signatures.");
|
||||
SigningModulusOverride = Config.Bind("Signing", "Signing Modulus Override", "", "Optional alternative to disabling verification: your own RSA public modulus, base64, RAW 2048-bit (256 bytes decoded) — NOT a PEM/DER key. When set, it is substituted for the modulus baked into global-metadata.dat and real verification still runs, so images stay signed with your keypair. Redundant while 'Disable Signature Verification' is true. Empty = leave the stock modulus alone.");
|
||||
|
||||
Harmony.CreateAndPatchAll(typeof(Plugin).Assembly);
|
||||
|
||||
SceneManager.sceneLoaded += (Action<Scene, LoadSceneMode>)OnSceneLoaded;
|
||||
|
||||
@@ -121,6 +121,14 @@ Inside `config`, edit the `net.rec.plugin.cfg` file and update as needed:
|
||||
- `App Id Voice` — Photon Voice App ID.
|
||||
- `App Id Chat` — Photon Chat App ID.
|
||||
|
||||
**[Signing]**
|
||||
- `Disable Signature Verification` — stops the client checking that images are signed with Rec Room's
|
||||
private key, so your own server can serve images. **On by default**; leave it alone unless you
|
||||
specifically want signed images.
|
||||
- `Signing Modulus Override` — only for setups that *do* want image signing: your own RSA public
|
||||
modulus (base64, raw 2048-bit — not a PEM/DER key). Keeps real verification, against your keypair.
|
||||
Leave empty otherwise.
|
||||
|
||||
**[Advanced]**
|
||||
- `Enabled Advanced Settings` — must be `true` to apply the custom Photon name server / port below.
|
||||
- `Photon NameServer` — custom Photon name server host.
|
||||
|
||||
Reference in New Issue
Block a user