Remove galvanic authentication support
... that server is being rewritten
This commit is contained in:
@@ -11,6 +11,7 @@ namespace undead_universal_patch_il2cpp.Core
|
|||||||
public static ConfigEntry<bool> SignalRHandshakeFix;
|
public static ConfigEntry<bool> SignalRHandshakeFix;
|
||||||
public static ConfigEntry<bool> ImageSignaturePatch;
|
public static ConfigEntry<bool> ImageSignaturePatch;
|
||||||
public static ConfigEntry<bool> RegistrationPatch;
|
public static ConfigEntry<bool> RegistrationPatch;
|
||||||
|
public static ConfigEntry<bool> AFKPatch;
|
||||||
}
|
}
|
||||||
public static class GenericConfigDefaults
|
public static class GenericConfigDefaults
|
||||||
{
|
{
|
||||||
@@ -21,6 +22,7 @@ namespace undead_universal_patch_il2cpp.Core
|
|||||||
public static bool SignalRHandshakeFix = false;
|
public static bool SignalRHandshakeFix = false;
|
||||||
public static bool ImageSignaturePatch = false;
|
public static bool ImageSignaturePatch = false;
|
||||||
public static bool RegistrationPatch = false;
|
public static bool RegistrationPatch = false;
|
||||||
|
public static bool AFKPatch = false;
|
||||||
}
|
}
|
||||||
public static class AssetChangesConfig
|
public static class AssetChangesConfig
|
||||||
{
|
{
|
||||||
@@ -70,21 +72,4 @@ namespace undead_universal_patch_il2cpp.Core
|
|||||||
public static string ServerAddress = "127.0.0.1";
|
public static string ServerAddress = "127.0.0.1";
|
||||||
public static int ServerPort = 5055;
|
public static int ServerPort = 5055;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class GalvanicConfig
|
|
||||||
{
|
|
||||||
public static ConfigEntry<bool> Enabled;
|
|
||||||
public static ConfigEntry<bool> RegenerateKeypair;
|
|
||||||
public static ConfigEntry<string> Export;
|
|
||||||
public static ConfigEntry<bool> Import;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class GalvanicConfigDefaults
|
|
||||||
{
|
|
||||||
public static bool Enabled = false;
|
|
||||||
public static bool RegenerateKeypair = false;
|
|
||||||
public static string Export = "IDoNotWantToExportMyKeys";
|
|
||||||
public static bool Import = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
239
Core/Galvanic.cs
239
Core/Galvanic.cs
@@ -1,239 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
using UnityEngine;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
// this entire file could be better
|
|
||||||
|
|
||||||
namespace undead_universal_patch_il2cpp.Core
|
|
||||||
{
|
|
||||||
public class GalvanicAuth
|
|
||||||
{
|
|
||||||
public static void PrepareKeys()
|
|
||||||
{
|
|
||||||
PlayerPrefs.DeleteKey("GalvanicPublicKey");
|
|
||||||
PlayerPrefs.DeleteKey("GalvanicPrivateKey");
|
|
||||||
using ECDsa ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
|
||||||
byte[] publicKeyBytes = ecdsa.ExportSubjectPublicKeyInfo();
|
|
||||||
string publicKeyBase64 = Convert.ToBase64String(publicKeyBytes);
|
|
||||||
|
|
||||||
byte[] privateKeyBytes = ecdsa.ExportPkcs8PrivateKey();
|
|
||||||
string privateKeyBase64 = Convert.ToBase64String(privateKeyBytes);
|
|
||||||
|
|
||||||
PlayerPrefs.SetString("GalvanicPublicKey", publicKeyBase64);
|
|
||||||
PlayerPrefs.SetString("GalvanicPrivateKey", privateKeyBase64);
|
|
||||||
PlayerPrefs.Save();
|
|
||||||
|
|
||||||
Plugin.Log.LogInfo("Created new keypair for Galvanic authentication.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetPubKey()
|
|
||||||
{
|
|
||||||
string publicKey = PlayerPrefs.GetString("GalvanicPublicKey", null);
|
|
||||||
if (string.IsNullOrEmpty(publicKey))
|
|
||||||
{
|
|
||||||
Plugin.Log.LogInfo("No keypair found, generating new.");
|
|
||||||
PrepareKeys();
|
|
||||||
return GetPubKey();
|
|
||||||
} else return publicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ECDsa GetPrivKey()
|
|
||||||
{
|
|
||||||
string privateKey = PlayerPrefs.GetString("GalvanicPrivateKey", null);
|
|
||||||
if (string.IsNullOrEmpty(privateKey))
|
|
||||||
{
|
|
||||||
Plugin.Log.LogInfo("No keypair found, generating new.");
|
|
||||||
PrepareKeys();
|
|
||||||
return GetPrivKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] privKeyBytes = Convert.FromBase64String(privateKey);
|
|
||||||
ECDsa crypt = ECDsa.Create();
|
|
||||||
crypt.ImportPkcs8PrivateKey(privKeyBytes, out _);
|
|
||||||
return crypt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string SignPayload(string message)
|
|
||||||
{
|
|
||||||
using ECDsa ecdsa = GetPrivKey();
|
|
||||||
if (ecdsa == null) return null;
|
|
||||||
|
|
||||||
byte[] payloadBytes = Encoding.UTF8.GetBytes(message);
|
|
||||||
byte[] signature = ecdsa.SignData(payloadBytes, HashAlgorithmName.SHA256);
|
|
||||||
|
|
||||||
return Convert.ToBase64String(signature);
|
|
||||||
}
|
|
||||||
public static void Export()
|
|
||||||
{
|
|
||||||
File.WriteAllText("./galvanic_keys_export.txt", $"{GetPubKey()}\n{Convert.ToBase64String(GetPrivKey().ExportPkcs8PrivateKey())}");
|
|
||||||
Plugin.Log.LogWarning("Galvanic Authentication keys were exported.");
|
|
||||||
}
|
|
||||||
public static void Import()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string imported = File.ReadAllText("./galvanic_keys_export.txt").ToString();
|
|
||||||
string privkey = imported.Split("\n")[1];
|
|
||||||
string pubkey = imported.Split("\n")[0];
|
|
||||||
if (privkey == null || pubkey == null) throw new Exception("Either imported key was null");
|
|
||||||
|
|
||||||
PlayerPrefs.SetString("GalvanicPrivateKey", privkey);
|
|
||||||
PlayerPrefs.SetString("GalvanicPublicKey", pubkey);
|
|
||||||
PlayerPrefs.Save();
|
|
||||||
} catch (Exception err)
|
|
||||||
{
|
|
||||||
Plugin.Log.LogError($"Could not import Galvanic Authentication keys: {err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public static ServerInfoRes GetServerInfo()
|
|
||||||
{
|
|
||||||
UriBuilder nameserver = new(NameserverConfig.NewUrl.Value);
|
|
||||||
nameserver.Path = "/info";
|
|
||||||
nameserver.Query = "";
|
|
||||||
|
|
||||||
using HttpClient client = new();
|
|
||||||
var response = client.GetAsync(nameserver.ToString()).Result;
|
|
||||||
if (!response.IsSuccessStatusCode) return null;
|
|
||||||
|
|
||||||
var str = response.Content.ReadAsStringAsync().Result;
|
|
||||||
var res = JsonSerializer.Deserialize<ServerInfoRes>(str);
|
|
||||||
|
|
||||||
GalvanicCorrosion.Name = res.name;
|
|
||||||
GalvanicCorrosion.Id = res.id;
|
|
||||||
GalvanicCorrosion.Motd = res.motd;
|
|
||||||
GalvanicCorrosion.Patches = res.patches;
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class GalvanicCorrosion
|
|
||||||
{
|
|
||||||
public static string Name { get; set; } = "Galvanic Corrosion";
|
|
||||||
public static string Id { get; set; } = "galvanic-corrosion-default";
|
|
||||||
public static string Motd { get; set; } = "A Galvanic Corrosion server.";
|
|
||||||
public static string[] Patches { get; set; } = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class GalvanicWebAuth
|
|
||||||
{
|
|
||||||
public static string Token { get; private set; } = null;
|
|
||||||
public static void TokenExpiry()
|
|
||||||
{
|
|
||||||
string url = NameserverConfig.NewUrl.Value;
|
|
||||||
UriBuilder uri = new(url);
|
|
||||||
uri.Path = "/user/checkExpired";
|
|
||||||
uri.Query = "";
|
|
||||||
|
|
||||||
HttpClient client = new();
|
|
||||||
client.DefaultRequestHeaders.Add("GalvanicAuth", Token);
|
|
||||||
HttpResponseMessage res = client.GetAsync(uri.ToString()).Result;
|
|
||||||
if (res.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
bool expired = JsonSerializer.Deserialize<bool>(res.Content.ReadAsStringAsync().Result.ToString());
|
|
||||||
if (expired) GetToken();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void GetToken()
|
|
||||||
{
|
|
||||||
string url = NameserverConfig.NewUrl.Value;
|
|
||||||
UriBuilder uri = new(url)
|
|
||||||
{
|
|
||||||
Path = "/user/auth",
|
|
||||||
Query = ""
|
|
||||||
};
|
|
||||||
|
|
||||||
var info = GalvanicAuth.GetServerInfo();
|
|
||||||
Plugin.Log.LogInfo($"Sending authentication request to server ID '{info.id}'");
|
|
||||||
if (uri.Scheme == "http") Plugin.Log.LogWarning("The server is not secure! Please use HTTPS.");
|
|
||||||
|
|
||||||
UserAuthPayload payload = new()
|
|
||||||
{
|
|
||||||
timestamp = ((DateTimeOffset)DateTime.Now).ToUnixTimeSeconds(),
|
|
||||||
nonce = Guid.NewGuid().ToString(),
|
|
||||||
server_id = info.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
UserAuthRequest request = new()
|
|
||||||
{
|
|
||||||
message = payload,
|
|
||||||
signature = GalvanicAuth.SignPayload($"{JsonSerializer.Serialize(payload)}"),
|
|
||||||
client_id = UniqueID.GetUniqueId(),
|
|
||||||
pubkey = GalvanicAuth.GetPubKey()
|
|
||||||
};
|
|
||||||
|
|
||||||
HttpClient client = new();
|
|
||||||
HttpResponseMessage res = client.PostAsync(uri.ToString(), new StringContent(JsonSerializer.Serialize(request,
|
|
||||||
new JsonSerializerOptions {
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
|
||||||
}),
|
|
||||||
Encoding.UTF8,
|
|
||||||
"application/json")).Result;
|
|
||||||
if (!res.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
Plugin.Log.LogError("The GalvanicWeb auth response was not successful.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
string str = res.Content.ReadAsStringAsync().Result;
|
|
||||||
TokenRes token = JsonSerializer.Deserialize<TokenRes>(str);
|
|
||||||
|
|
||||||
Token = token.token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class UniqueID
|
|
||||||
{
|
|
||||||
public static string GetUniqueId()
|
|
||||||
{
|
|
||||||
if (!PlayerPrefs.HasKey("UniqueId")) SaveUniqueId(GenerateUniqueId());
|
|
||||||
return PlayerPrefs.GetString("UniqueId");
|
|
||||||
}
|
|
||||||
public static void SaveUniqueId(string id)
|
|
||||||
{
|
|
||||||
PlayerPrefs.SetString("UniqueId", id);
|
|
||||||
PlayerPrefs.Save();
|
|
||||||
}
|
|
||||||
public static string GenerateUniqueId()
|
|
||||||
{
|
|
||||||
byte[] buffer = new byte[128];
|
|
||||||
new System.Random().NextBytes(buffer);
|
|
||||||
return Convert.ToBase64String(buffer).Substring(0, 128);
|
|
||||||
|
|
||||||
// god help me
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ServerInfoRes
|
|
||||||
{
|
|
||||||
public string name { get; set; }
|
|
||||||
public string id { get; set; }
|
|
||||||
public string motd { get; set; }
|
|
||||||
public string[] patches { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
class TokenRes
|
|
||||||
{
|
|
||||||
public string token { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
class UserAuthPayload
|
|
||||||
{
|
|
||||||
public long timestamp { get; set; }
|
|
||||||
public string nonce { get; set; }
|
|
||||||
public string server_id { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
class UserAuthRequest
|
|
||||||
{
|
|
||||||
public string client_id { get; set; }
|
|
||||||
public UserAuthPayload message { get; set; }
|
|
||||||
public string signature { get; set; }
|
|
||||||
public string pubkey { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
30
Patches/AFKPatch.cs
Normal file
30
Patches/AFKPatch.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using HarmonyLib;
|
||||||
|
using undead_universal_patch_il2cpp.Core;
|
||||||
|
|
||||||
|
namespace undead_universal_patch_il2cpp.Patches;
|
||||||
|
|
||||||
|
[HarmonyPatch]
|
||||||
|
public class AFKPatch
|
||||||
|
{
|
||||||
|
static readonly string TargetTypeName = "Player";
|
||||||
|
static readonly string TargetMethodName = "UpdateAFKStatus";
|
||||||
|
static readonly string Description = "Always present, never AFK";
|
||||||
|
|
||||||
|
static bool Prepare()
|
||||||
|
{
|
||||||
|
if (AccessTools.Method(AccessTools.TypeByName(TargetTypeName), TargetMethodName) == null)
|
||||||
|
{
|
||||||
|
Plugin.Log.LogWarning($"'{Description}' disabled. The method for this patch was not found.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Plugin.Log.LogInfo($"'{Description}' succeeded validation.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static MethodBase TargetMethod() => AccessTools.Method(AccessTools.TypeByName(TargetTypeName), TargetMethodName);
|
||||||
|
static void Prefix(ref bool userIsPresent)
|
||||||
|
{
|
||||||
|
if (GenericConfig.AFKPatch.Value) userIsPresent = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,30 +64,6 @@ namespace undead_universal_patch_il2cpp.Patches
|
|||||||
|
|
||||||
if (newUri.ToString().Contains("ns.rec.net")) newUri = new Il2CppSystem.Uri(NameserverConfig.NewUrl.Value);
|
if (newUri.ToString().Contains("ns.rec.net")) newUri = new Il2CppSystem.Uri(NameserverConfig.NewUrl.Value);
|
||||||
|
|
||||||
if (GalvanicConfig.Enabled.Value)
|
|
||||||
{
|
|
||||||
string[] applyHeader = [
|
|
||||||
"/cachedlogin/forplatformid",
|
|
||||||
"/account/create",
|
|
||||||
"/connect/token"
|
|
||||||
];
|
|
||||||
foreach (string header in applyHeader)
|
|
||||||
{
|
|
||||||
if (newUri.PathAndQuery.Contains(header))
|
|
||||||
{
|
|
||||||
// refresh the token if it expired
|
|
||||||
// this is somewhat inefficient, but we don't hook into many requests (see above) so it should be fine
|
|
||||||
GalvanicWebAuth.TokenExpiry();
|
|
||||||
|
|
||||||
Type httpRequestType = request.GetType();
|
|
||||||
MethodInfo addHeaderMethod = httpRequestType.GetMethod("AddHeader");
|
|
||||||
addHeaderMethod.Invoke(request, ["GalvanicAuth", GalvanicWebAuth.Token]);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (GenericConfig.LogAllRequests.Value) Plugin.Log.LogInfo($"BestHTTP_Unob request a-URL: {newUri.ToString()}");
|
if (GenericConfig.LogAllRequests.Value) Plugin.Log.LogInfo($"BestHTTP_Unob request a-URL: {newUri.ToString()}");
|
||||||
uriProperty.SetValue(request, NameserverConfig.Rewrite.Value ? newUri : uriInstance, null);
|
uriProperty.SetValue(request, NameserverConfig.Rewrite.Value ? newUri : uriInstance, null);
|
||||||
}
|
}
|
||||||
|
|||||||
26
Plugin.cs
26
Plugin.cs
@@ -49,6 +49,8 @@ public class Plugin : BasePlugin
|
|||||||
"\nWorks only if the server appends a properly formatted signature header (signature does not need to be valid)");
|
"\nWorks only if the server appends a properly formatted signature header (signature does not need to be valid)");
|
||||||
GenericConfig.RegistrationPatch = Config.Bind("Generic", "RegistrationPatch", GenericConfigDefaults.RegistrationPatch,
|
GenericConfig.RegistrationPatch = Config.Bind("Generic", "RegistrationPatch", GenericConfigDefaults.RegistrationPatch,
|
||||||
"Always disable the registration prompt.");
|
"Always disable the registration prompt.");
|
||||||
|
GenericConfig.AFKPatch = Config.Bind("Generic", "AFKPatch", GenericConfigDefaults.AFKPatch,
|
||||||
|
"Always present patch. Never get kicked to dorm.");
|
||||||
|
|
||||||
AssetChangesConfig.AGRoomChanges = Config.Bind("AssetChangesConfig", "AGRoomChanges", AssetChangesConfigDefaults.AGRoomChanges,
|
AssetChangesConfig.AGRoomChanges = Config.Bind("AssetChangesConfig", "AGRoomChanges", AssetChangesConfigDefaults.AGRoomChanges,
|
||||||
$"Use patches from '{AGRoomChanges.config.path}' to change the properties of internal AGRooms. See README.md");
|
$"Use patches from '{AGRoomChanges.config.path}' to change the properties of internal AGRooms. See README.md");
|
||||||
@@ -80,31 +82,7 @@ public class Plugin : BasePlugin
|
|||||||
NameserverConfig.NewUrl = Config.Bind("Nameserver", "NewUrl", NameserverConfigDefaults.NewUrl,
|
NameserverConfig.NewUrl = Config.Bind("Nameserver", "NewUrl", NameserverConfigDefaults.NewUrl,
|
||||||
"The new full URL to use when sending a nameserver request.");
|
"The new full URL to use when sending a nameserver request.");
|
||||||
|
|
||||||
GalvanicConfig.Enabled = Config.Bind("GalvanicCorrosion", "Enabled", GalvanicConfigDefaults.Enabled,
|
|
||||||
"Use Galvanic Corrosion features. Authenticates with a GC server." +
|
|
||||||
"\nThe host and protocol in Nameserver.NewUrl will be used for the server's base URL." +
|
|
||||||
"\nDo not enable if you either don't know what this does.");
|
|
||||||
GalvanicConfig.RegenerateKeypair = Config.Bind("GalvanicCorrosion", "RegenerateKeypair", GalvanicConfigDefaults.RegenerateKeypair,
|
|
||||||
"Regenerate the keypair upon startup. DO NOT ENABLE IF YOU WANT TO KEEP YOUR ACCOUNT.");
|
|
||||||
GalvanicConfig.Export = Config.Bind("GalvanicCorrosion", "Export", GalvanicConfigDefaults.Export,
|
|
||||||
"Export the keypair to a plaintext file in the game directory. Set to 'IWantToExportMyKeys' to enable." +
|
|
||||||
"\n**DO NOT ENABLE IF YOU DO NOT KNOW WHAT THIS DOES.**" +
|
|
||||||
"\n**DO NOT ENABLE THIS IF SOMEONE UNTRUSTWORTHY TOLD YOU TO.**" +
|
|
||||||
"\n**THESE KEYS CONTAIN THE CREDENTIALS THAT GRANT ACCESS TO YOUR ACCOUNTS.**");
|
|
||||||
GalvanicConfig.Import = Config.Bind("GalvanicCorrosion", "Import", GalvanicConfigDefaults.Import,
|
|
||||||
"Import the Galvanic Authentication keys from a file." +
|
|
||||||
"\nBe sure to not enable this at the same time as `Export`, as that will cause the keys to" +
|
|
||||||
"\nbe unnecessarily written and read to and from the disk.");
|
|
||||||
|
|
||||||
_hi.PatchAll();
|
_hi.PatchAll();
|
||||||
if (GalvanicConfig.Enabled.Value && GalvanicConfig.RegenerateKeypair.Value)
|
|
||||||
{
|
|
||||||
Log.LogInfo("Regenerating keypair");
|
|
||||||
GalvanicAuth.PrepareKeys();
|
|
||||||
}
|
|
||||||
if (GalvanicConfig.Enabled.Value && GalvanicConfig.Export.Value == "IWantToExportMyKeys") GalvanicAuth.Export();
|
|
||||||
if (GalvanicConfig.Enabled.Value && GalvanicConfig.Import.Value) GalvanicAuth.Import();
|
|
||||||
if (GalvanicConfig.Enabled.Value) GalvanicWebAuth.GetToken();
|
|
||||||
|
|
||||||
Log.LogInfo("PATCH LIST START =========================");
|
Log.LogInfo("PATCH LIST START =========================");
|
||||||
foreach (var method in _hi.GetPatchedMethods()) Log.LogInfo($"- {method.ToString()}");
|
foreach (var method in _hi.GetPatchedMethods()) Log.LogInfo($"- {method.ToString()}");
|
||||||
|
|||||||
@@ -37,11 +37,6 @@ Run BepInEx interop on your build and add the following assemblies to new folder
|
|||||||
|
|
||||||
### Linux users
|
### Linux users
|
||||||
|
|
||||||
Wine currently has problems generating the keypair used in Galvanic
|
|
||||||
authentication.
|
|
||||||
|
|
||||||
You can use a real Windows system to generate the keys for the first startup.
|
|
||||||
|
|
||||||
If you're unsure how to start your build on linux:
|
If you're unsure how to start your build on linux:
|
||||||
|
|
||||||
- Install Steam
|
- Install Steam
|
||||||
|
|||||||
Reference in New Issue
Block a user