fix(plugin): isolate login preferences by server origin and preserve official credentials
This commit is contained in:
@@ -65,6 +65,7 @@ internal sealed class ClientRouting : IDisposable
|
||||
|
||||
private readonly ManualLogSource log;
|
||||
private readonly ClientConfig config;
|
||||
internal string ServerOrigin => config.server_origin;
|
||||
private readonly ResourcePolicy resources;
|
||||
private readonly LocalResourceServer localResourceServer;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<BD2GameVersion>2.35.10</BD2GameVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="../Shared/ServerLoginPreferences.cs" Link="ServerLoginPreferences.cs" />
|
||||
<Reference Include="BepInEx">
|
||||
<HintPath>$(BD2BepInExDir)\core\BepInEx.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using BD2.GameNames;
|
||||
using Bd2Login;
|
||||
using BepInEx.Logging;
|
||||
using HarmonyLib;
|
||||
using gamfs.Platform;
|
||||
|
||||
namespace Bd2LocalIdentity;
|
||||
|
||||
internal static class LocalLoginState
|
||||
{
|
||||
private static string Origin;
|
||||
|
||||
internal static void Install(Harmony harmony, string serverOrigin, ManualLogSource log)
|
||||
{
|
||||
Origin = ServerLoginPreferences.NormalizeOrigin(new Uri(serverOrigin));
|
||||
PropertyInfo automatic = typeof(PlatformManager).GetGameProperty(
|
||||
nameof(PlatformManager.IsAutoLogin), BindingFlags.Public | BindingFlags.Instance);
|
||||
PropertyInfo checkbox = typeof(PlatformManager).GetGameProperty(
|
||||
nameof(PlatformManager.UseAutoLoginPC), BindingFlags.Public | BindingFlags.Instance);
|
||||
MethodInfo accessToken = Game.Getter(() => BDNetwork.CommonPacket.AccessToken);
|
||||
Validate(automatic);
|
||||
Validate(checkbox);
|
||||
if (accessToken == null) throw new MissingMethodException("CommonPacket.AccessToken getter was not found");
|
||||
|
||||
harmony.Patch(automatic.GetGetMethod(), prefix: new HarmonyMethod(typeof(LocalLoginState), nameof(AutoLoginGetter)));
|
||||
harmony.Patch(automatic.GetSetMethod(), prefix: new HarmonyMethod(typeof(LocalLoginState), nameof(AutoLoginSetter)));
|
||||
harmony.Patch(checkbox.GetGetMethod(), prefix: new HarmonyMethod(typeof(LocalLoginState), nameof(CheckboxGetter)));
|
||||
harmony.Patch(checkbox.GetSetMethod(), prefix: new HarmonyMethod(typeof(LocalLoginState), nameof(CheckboxSetter)));
|
||||
// Skip the original getter's PlayerPrefs access. LoginUI's postfix
|
||||
// supplies its in-memory OAuth token when installed; this value also
|
||||
// supports LocalIdentity alone without writing the official token key.
|
||||
harmony.Patch(accessToken, prefix: new HarmonyMethod(typeof(LocalLoginState), nameof(BootstrapToken)));
|
||||
log.LogInfo("Login preferences isolated by server origin; official login keys are untouched");
|
||||
}
|
||||
|
||||
private static void Validate(PropertyInfo property)
|
||||
{
|
||||
if (property == null || property.PropertyType != typeof(bool) ||
|
||||
property.GetGetMethod() == null || property.GetSetMethod() == null)
|
||||
throw new MissingMemberException("PlatformManager auto-login property was not found (client version mismatch)");
|
||||
}
|
||||
|
||||
private static bool AutoLoginGetter(ref bool __result)
|
||||
{
|
||||
__result = ServerLoginPreferences.IsAutoLogin(Origin);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool AutoLoginSetter(bool __0)
|
||||
{
|
||||
ServerLoginPreferences.SetAutoLogin(Origin, __0);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool CheckboxGetter(ref bool __result)
|
||||
{
|
||||
__result = ServerLoginPreferences.UseAutoLoginPC(Origin);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool CheckboxSetter(bool __0)
|
||||
{
|
||||
ServerLoginPreferences.SetUseAutoLoginPC(Origin, __0);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool BootstrapToken(ref string __result)
|
||||
{
|
||||
__result = "bd2-local-development-user";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -31,11 +31,6 @@ public sealed class Plugin : BaseUnityPlugin
|
||||
{
|
||||
Log = Logger;
|
||||
Game.Validate(typeof(Plugin).Assembly, Bd2Build.Versions.Game, message => Logger.LogInfo(message));
|
||||
// The non-SDK branch still needs a local bootstrap identity for
|
||||
// MaintenanceInfo. OAuth LoginUI replaces this value after its
|
||||
// browser/device transaction completes.
|
||||
PlayerPrefs.SetString("AccessToken", "bd2-local-development-user");
|
||||
PlayerPrefs.Save();
|
||||
Routing = ClientRouting.Load(Logger);
|
||||
if (Interlocked.Exchange(ref ShutdownHooksInstalled, 1) == 0)
|
||||
{
|
||||
@@ -53,6 +48,7 @@ public sealed class Plugin : BaseUnityPlugin
|
||||
nameof(UseSdkPrefix),
|
||||
BindingFlags.Static | BindingFlags.NonPublic);
|
||||
Harmony harmony = new Harmony(Guid);
|
||||
LocalLoginState.Install(harmony, Routing.ServerOrigin, Logger);
|
||||
harmony.Patch(getter, prefix: new HarmonyMethod(prefix));
|
||||
TryInstall("OS time zone device country", () => SystemTimeZoneRegion.Install(harmony, Logger));
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using Bd2Login;
|
||||
|
||||
namespace Bd2LoginUI;
|
||||
|
||||
@@ -180,24 +181,7 @@ internal sealed class WindowsDpapiRefreshCredentialStore : IRefreshCredentialSto
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
private static string PreferenceFor(string origin)
|
||||
{
|
||||
byte[] input = Encoding.UTF8.GetBytes(origin);
|
||||
byte[] digest;
|
||||
using (SHA256 sha = SHA256.Create())
|
||||
{
|
||||
digest = sha.ComputeHash(input);
|
||||
}
|
||||
try
|
||||
{
|
||||
return PreferencePrefix + Hex(digest);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Clear(input);
|
||||
Clear(digest);
|
||||
}
|
||||
}
|
||||
private static string PreferenceFor(string origin) => ServerLoginPreferences.Key(PreferencePrefix, origin);
|
||||
|
||||
private static byte[] Entropy(string origin)
|
||||
{
|
||||
@@ -306,16 +290,6 @@ internal sealed class WindowsDpapiRefreshCredentialStore : IRefreshCredentialSto
|
||||
}
|
||||
}
|
||||
|
||||
private static string Hex(byte[] value)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder(value.Length * 2);
|
||||
foreach (byte item in value)
|
||||
{
|
||||
builder.Append(item.ToString("x2"));
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static void Clear(byte[] bytes)
|
||||
{
|
||||
if (bytes != null)
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using BD2.GameNames;
|
||||
using Bd2Login;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
@@ -32,15 +33,14 @@ internal static class LoginController
|
||||
internal static bool LoginInProgress;
|
||||
internal static MemoryAccessTokenStore AccessTokens;
|
||||
internal static IRefreshCredentialStore RefreshCredentials;
|
||||
internal static bool AccessTokenPrefix(ref string __result)
|
||||
internal static void AccessTokenPostfix(ref string __result)
|
||||
{
|
||||
if (Authentication != null && Authentication.mode == "oauth")
|
||||
{
|
||||
__result = AccessTokens.Get();
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
__result = LocalAccessToken;
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static void ClearPCLocalDataPostfix()
|
||||
@@ -49,9 +49,7 @@ internal static class LoginController
|
||||
EstablishedGameSession = false;
|
||||
RuntimeProbeFailures = 0;
|
||||
ServerInstanceID = null;
|
||||
PlayerPrefs.DeleteKey("AccessToken");
|
||||
DeleteCurrentRefresh();
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
internal static bool SendMaintenancePrefix(object __instance, bool __0)
|
||||
@@ -79,8 +77,6 @@ internal static class LoginController
|
||||
DisposeGameRelay();
|
||||
ServerRoot = currentRoot;
|
||||
EnsureGameRelay();
|
||||
PlayerPrefs.DeleteKey("AccessToken");
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
if (Volatile.Read(ref SessionRecoveryInProgress) != 0 && Authentication != null &&
|
||||
Authentication.mode == "oauth" && AccessTokens.IsUsable(NormalizedServerOrigin()))
|
||||
@@ -169,8 +165,6 @@ internal static class LoginController
|
||||
try
|
||||
{
|
||||
AccessTokens.Clear();
|
||||
PlayerPrefs.DeleteKey("AccessToken");
|
||||
PlayerPrefs.Save();
|
||||
ContinueMaintenance = true;
|
||||
SendMaintenance.Invoke(introUI, new object[] { true });
|
||||
}
|
||||
@@ -181,17 +175,12 @@ internal static class LoginController
|
||||
return;
|
||||
}
|
||||
ValidateOAuthTransport(ServerRoot);
|
||||
// Earlier development builds stored both local identifiers and OAuth
|
||||
// access credentials in this key. OAuth credentials now live only in
|
||||
// process memory, so remove any legacy plaintext before proceeding.
|
||||
PlayerPrefs.DeleteKey("AccessToken");
|
||||
if (!RefreshCredentials.IsSupported)
|
||||
{
|
||||
PlayerPrefs.SetInt("IsAutoLogin", 0);
|
||||
PlayerPrefs.SetInt("StandaloneAutoLogin", 0);
|
||||
ServerLoginPreferences.SetAutoLogin(NormalizedServerOrigin(), false);
|
||||
ServerLoginPreferences.SetUseAutoLoginPC(NormalizedServerOrigin(), false);
|
||||
Log?.LogWarning("Secure refresh credential storage is unavailable; automatic login is disabled on this platform");
|
||||
}
|
||||
PlayerPrefs.Save();
|
||||
if (LoginInProgress)
|
||||
{
|
||||
return;
|
||||
@@ -213,8 +202,8 @@ internal static class LoginController
|
||||
ShowLoginPanel(introUI);
|
||||
return;
|
||||
}
|
||||
if (PlayerPrefs.GetInt("IsAutoLogin", 0) != 0 &&
|
||||
PlayerPrefs.GetInt("StandaloneAutoLogin", 0) != 0 &&
|
||||
if (ServerLoginPreferences.IsAutoLogin(NormalizedServerOrigin()) &&
|
||||
ServerLoginPreferences.UseAutoLoginPC(NormalizedServerOrigin()) &&
|
||||
CanAttemptAutomaticLogin())
|
||||
{
|
||||
LoginInProgress = true;
|
||||
@@ -375,10 +364,8 @@ internal static class LoginController
|
||||
AccessTokens.Set(result.access_token, NormalizedServerOrigin(), result.provider, result.access_expires_in);
|
||||
result.access_token = null;
|
||||
result.refresh_token = null;
|
||||
PlayerPrefs.SetInt("IsAutoLogin", 0);
|
||||
PlayerPrefs.SetInt("StandaloneAutoLogin", 0);
|
||||
PlayerPrefs.DeleteKey("AccessToken");
|
||||
PlayerPrefs.Save();
|
||||
ServerLoginPreferences.SetAutoLogin(NormalizedServerOrigin(), false);
|
||||
ServerLoginPreferences.SetUseAutoLoginPC(NormalizedServerOrigin(), false);
|
||||
Log?.LogWarning("Login succeeded, but automatic login remains disabled because this platform has no supported secure credential store");
|
||||
ContinueWithMaintenance(introUI, false);
|
||||
return;
|
||||
@@ -393,7 +380,7 @@ internal static class LoginController
|
||||
}
|
||||
try
|
||||
{
|
||||
bool autoLogin = PlayerPrefs.GetInt("StandaloneAutoLogin", 0) != 0;
|
||||
bool autoLogin = ServerLoginPreferences.UseAutoLoginPC(NormalizedServerOrigin());
|
||||
if (autoLogin)
|
||||
{
|
||||
StoreRefresh(result);
|
||||
@@ -405,9 +392,7 @@ internal static class LoginController
|
||||
}
|
||||
AccessTokens.Set(result.access_token, NormalizedServerOrigin(), result.provider, result.access_expires_in);
|
||||
result.access_token = null;
|
||||
PlayerPrefs.SetInt("IsAutoLogin", autoLogin ? 1 : 0);
|
||||
PlayerPrefs.DeleteKey("AccessToken");
|
||||
PlayerPrefs.Save();
|
||||
ServerLoginPreferences.SetAutoLogin(NormalizedServerOrigin(), autoLogin);
|
||||
ContinueWithMaintenance(introUI, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -557,8 +542,6 @@ internal static class LoginController
|
||||
}
|
||||
AccessTokens.Set(result.access_token, NormalizedServerOrigin(), result.provider, result.access_expires_in);
|
||||
result.access_token = null;
|
||||
PlayerPrefs.DeleteKey("AccessToken");
|
||||
PlayerPrefs.Save();
|
||||
refreshToken = null;
|
||||
attemptID = null;
|
||||
}
|
||||
@@ -705,11 +688,9 @@ internal static class LoginController
|
||||
private static void ClearSavedLogin()
|
||||
{
|
||||
AccessTokens.Clear();
|
||||
PlayerPrefs.SetInt("IsAutoLogin", 0);
|
||||
PlayerPrefs.SetInt("StandaloneAutoLogin", 0);
|
||||
PlayerPrefs.DeleteKey("AccessToken");
|
||||
ServerLoginPreferences.SetAutoLogin(NormalizedServerOrigin(), false);
|
||||
ServerLoginPreferences.SetUseAutoLoginPC(NormalizedServerOrigin(), false);
|
||||
DeleteCurrentRefresh();
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
private static void DeleteCurrentRefresh()
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using BD2.GameNames;
|
||||
using Bd2Login;
|
||||
using BepInEx.Logging;
|
||||
|
||||
using static BD2.GameNames.Game;
|
||||
@@ -28,17 +29,7 @@ internal static class LoginRuntime
|
||||
return string.Equals(NormalizeOrigin(left), NormalizeOrigin(right), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string NormalizeOrigin(Uri uri)
|
||||
{
|
||||
if (uri == null || !uri.IsAbsoluteUri || string.IsNullOrEmpty(uri.Host))
|
||||
{
|
||||
throw new InvalidOperationException("authentication server origin is unavailable");
|
||||
}
|
||||
string host = uri.IdnHost.ToLowerInvariant();
|
||||
int port = uri.IsDefaultPort ? -1 : uri.Port;
|
||||
UriBuilder builder = new UriBuilder(uri.Scheme.ToLowerInvariant(), host, port);
|
||||
return builder.Uri.GetLeftPart(UriPartial.Authority).TrimEnd('/');
|
||||
}
|
||||
private static string NormalizeOrigin(Uri uri) => ServerLoginPreferences.NormalizeOrigin(uri);
|
||||
|
||||
internal static void ValidateOAuthTransport(Uri uri)
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<BD2GameVersion>2.35.10</BD2GameVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="../Shared/ServerLoginPreferences.cs" Link="ServerLoginPreferences.cs" />
|
||||
<Reference Include="BepInEx">
|
||||
<HintPath>$(BD2BepInExDir)\core\BepInEx.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
|
||||
@@ -97,7 +97,7 @@ public sealed class Plugin : BaseUnityPlugin
|
||||
harmony.Patch(SendMaintenance, prefix: maintenancePrefix);
|
||||
harmony.Patch(
|
||||
accessTokenGetter,
|
||||
prefix: new HarmonyMethod(typeof(LoginController), nameof(AccessTokenPrefix)));
|
||||
postfix: new HarmonyMethod(typeof(LoginController), nameof(AccessTokenPostfix)));
|
||||
harmony.Patch(
|
||||
clearPCLocalData,
|
||||
postfix: new HarmonyMethod(typeof(LoginController), nameof(ClearPCLocalDataPostfix)));
|
||||
|
||||
+3
-1
@@ -29,7 +29,9 @@ dotnet build .\plugins\CaptureEnvironment\CaptureEnvironment.csproj -c Release -
|
||||
|
||||
认证策略由服务端同目录的 `authentication.json` 决定。`mode=local` 保持本地自动登录;公网或联机服可设为 `oauth`,并在 `providers` 中开启 `discord`、`google` 或两者。LoginUI 从当前连接的服务器读取这项策略,客户端不能自行启用服务端未开放的 provider。
|
||||
|
||||
OAuth access token 只保存在客户端进程内存中。自动登录 refresh credential 按规范化服务器 origin 隔离:Windows 使用当前用户作用域的 DPAPI 加密,再把密文按 origin 哈希键写入 PlayerPrefs 对应的用户注册表;macOS 使用系统 Keychain Services。没有受支持安全凭据存储的平台会禁用自动登录,不会回退到明文文件或 PlayerPrefs。除 Windows 的 DPAPI 密文外,PlayerPrefs 只保存原版的 `IsAutoLogin` 与 `StandaloneAutoLogin` 非敏感选择。
|
||||
OAuth access token 只保存在客户端进程内存中。自动登录 refresh credential 按规范化服务器 origin 隔离:Windows 使用当前用户作用域的 DPAPI 加密,再把密文按 `BD2OAuthRefreshV1_<origin 哈希>` 键写入 PlayerPrefs 对应的用户注册表;macOS 使用系统 Keychain Services。没有受支持安全凭据存储的平台会禁用自动登录,不会回退到明文文件或 PlayerPrefs。
|
||||
|
||||
自动登录选择也按服务器 origin 隔离,使用 `BD2LoginV1_IsAutoLogin_<origin 哈希>` 和 `BD2LoginV1_StandaloneAutoLogin_<origin 哈希>`。LocalIdentity 接管原版 `PlatformManager.IsAutoLogin` 与 `UseAutoLoginPC` 的全部读写,原有勾选框、确认和清除登录流程使用这些独立键;LoginUI 使用同一套键。scheme、主机和非默认端口区分服务器,主机大小写、默认端口和尾部斜杠不产生不同状态。插件不读取、覆盖或删除官方 `IsAutoLogin`、`StandaloneAutoLogin`、`AccessToken`,LocalIdentity 的启动标识通过 getter 在内存中提供。旧版写入的官方键不自动迁移或清理,避免影响官方账号;升级后各服务器需要重新选择自动登录。
|
||||
|
||||
启动原版抓包环境:
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Bd2Login;
|
||||
|
||||
// Both plugins use the same normalized origin and key format. Never use the
|
||||
// official login preference keys, even before the authentication policy loads.
|
||||
internal static class ServerLoginPreferences
|
||||
{
|
||||
internal static string NormalizeOrigin(Uri uri)
|
||||
{
|
||||
if (uri == null || !uri.IsAbsoluteUri || string.IsNullOrEmpty(uri.Host) ||
|
||||
(uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps) || uri.UserInfo.Length != 0)
|
||||
throw new InvalidOperationException("authentication server origin is unavailable or invalid");
|
||||
var builder = new UriBuilder(uri.Scheme.ToLowerInvariant(), uri.IdnHost.ToLowerInvariant(),
|
||||
uri.IsDefaultPort ? -1 : uri.Port);
|
||||
return builder.Uri.GetLeftPart(UriPartial.Authority).TrimEnd('/');
|
||||
}
|
||||
|
||||
internal static string Key(string prefix, string origin)
|
||||
{
|
||||
string normalized = NormalizeOrigin(new Uri(origin, UriKind.Absolute));
|
||||
using SHA256 sha = SHA256.Create();
|
||||
byte[] digest = sha.ComputeHash(Encoding.UTF8.GetBytes(normalized));
|
||||
var hex = new StringBuilder(digest.Length * 2);
|
||||
foreach (byte value in digest) hex.Append(value.ToString("x2"));
|
||||
return prefix + hex;
|
||||
}
|
||||
|
||||
internal static bool IsAutoLogin(string origin) => Read("IsAutoLogin", origin);
|
||||
internal static bool UseAutoLoginPC(string origin) => Read("StandaloneAutoLogin", origin);
|
||||
internal static void SetAutoLogin(string origin, bool value) => Write("IsAutoLogin", origin, value);
|
||||
internal static void SetUseAutoLoginPC(string origin, bool value) => Write("StandaloneAutoLogin", origin, value);
|
||||
|
||||
private static bool Read(string setting, string origin) =>
|
||||
PlayerPrefs.GetInt(Key("BD2LoginV1_" + setting + "_", origin), 0) != 0;
|
||||
|
||||
private static void Write(string setting, string origin, bool value)
|
||||
{
|
||||
PlayerPrefs.SetInt(Key("BD2LoginV1_" + setting + "_", origin), value ? 1 : 0);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user