客户端rsa,冗余清理,bug修复,安卓build问题

This commit is contained in:
2026-07-14 01:43:49 +08:00
parent fd22501f71
commit f8985d91d2
682 changed files with 4595 additions and 11216 deletions
@@ -11,11 +11,7 @@ using UnityEngine;
using Bansonic;
using UnityEngine.SceneManagement;
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
#define ARENA_ROOM_DISABLE_STEAMWORKS
#endif
#if !ARENA_ROOM_DISABLE_STEAMWORKS
#if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX
using Steamworks;
#endif
@@ -2205,7 +2201,7 @@ namespace GameServer.Client
return network.SteamDisplayName ?? string.Empty;
}
#if ARENA_ROOM_DISABLE_STEAMWORKS
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
return string.Empty;
#else
if (SteamManager.Initialized && ulong.TryParse(senderSteamId, out ulong parsedSteamId))
@@ -1,6 +1,4 @@
using System;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using GameServer.Client;
@@ -115,8 +113,9 @@ public class GameServerBridge : MonoBehaviour
}
// ── HMAC 签名 ──
// 优先用握手下发的"每会话签名密钥";无会话密钥时回退到内置静态密钥(兼容旧服务端)。
string payload = $"{songId}|{difficulty}|{totalScore}|{chartScore}|{idolScore}";
string hmac = ComputeHmacSha256(payload, HMAC_SECRET);
string hmac = GameServer.Client.GameServerSession.ComputeScoreHmac(payload, HMAC_SECRET);
// ── 上传日志(清晰可见) ──
Debug.Log("╔══════════════════════════════════════════════════════╗");
@@ -169,17 +168,6 @@ public class GameServerBridge : MonoBehaviour
}
}
private static string ComputeHmacSha256(string payload, string secret)
{
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)))
{
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
var sb = new StringBuilder(hash.Length * 2);
foreach (byte b in hash) sb.Append(b.ToString("x2"));
return sb.ToString();
}
}
private static string ConvertDifficulty(int i) => i switch { 0 => "ez", 1 => "hd", 2 => "in", 3 => "im", _ => "unknown" };
private static string GetGrade(long s) => s >= 960000 ? "SSS" : s >= 920000 ? "SS" : s >= 880000 ? "S" : s >= 820000 ? "A" : s >= 720000 ? "B" : s >= 600000 ? "C" : s >= 400000 ? "D" : "F";
}
@@ -0,0 +1,261 @@
using System;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
namespace GameServer.Client
{
/// <summary>
/// 客户端会话状态:保存握手返回的会话令牌与"每会话签名密钥",
/// 并用内置的服务端 RSA 公钥验证会话密钥确实来自真实服务端(明文 HTTP 下防中间人伪造)。
///
/// 设计要点:
/// - session_token 用于后续请求的 Authorization: Bearer,服务端据此绑定 steam_id,杜绝伪造他人成绩。
/// - session_key 取代内置静态 HMAC 密钥。静态密钥内置于二进制、可被逆向提取;
/// 会话密钥每次握手随机、仅本会话有效,即使泄露也无法复用。
/// - server_signature 用内置公钥验签。公钥泄露无害(只能验签、不能签名)。
/// </summary>
public static class GameServerSession
{
// 服务端 RSA 公钥(SubjectPublicKeyInfo / PEM)。与服务器 keys/server_public.pem 对应。
// 公钥内置于客户端无安全风险:只能验签、不能签名。
// 留空时跳过验签(灰度期兼容:仍可用会话密钥,只是不做来源校验)。
private const string ServerPublicKeyPem = @"-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0lQb5hAQO7CfTTl2EScC
+YFt11TMEUO7n4aCwBEthWnXj8WM6M7yxbUrojmM3JcqdmaQtC59VTYIralhCtbV
cW6ACoi25vmArzc8QcOwjUPX3ZoorVp9jrVU0eIDo2qqii2eC/OKCw7bPOoIVG5A
ZqfI1CtmkMSDQD9lg0MDpDWWdGJfwGXAznhsERl9K2F42ZPAGU+qFEz4rTg4T+SX
oXT5PrFs0rlCjCUqoYo8s+doiFqxZGCUalk9k2h+e+uNSyVfwEopcZOcWghKlRpi
5VVkFpmDROIXOsVVcTeXWmwR94v6dqPztsyqkjP/S2K5GHtN7ilYFe0w0xAR84DL
ywIDAQAB
-----END PUBLIC KEY-----";
private static string _sessionToken = string.Empty;
private static string _sessionKey = string.Empty;
private static string _expiresAtUtc = string.Empty;
public static string SessionToken => _sessionToken;
public static string SessionKey => _sessionKey;
/// <summary>是否持有可用(未过期)的会话令牌。</summary>
public static bool HasValidToken
{
get
{
if (string.IsNullOrWhiteSpace(_sessionToken))
{
return false;
}
if (string.IsNullOrWhiteSpace(_expiresAtUtc))
{
return true;
}
if (DateTime.TryParse(
_expiresAtUtc,
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.AdjustToUniversal | System.Globalization.DateTimeStyles.AssumeUniversal,
out DateTime expires))
{
// 提前 60 秒判定过期,避免边界请求被服务端拒绝。
return DateTime.UtcNow < expires.AddSeconds(-60);
}
return true;
}
}
public static void Clear()
{
_sessionToken = string.Empty;
_sessionKey = string.Empty;
_expiresAtUtc = string.Empty;
}
/// <summary>
/// 应用握手响应。验签失败时不保存会话密钥(回退到 legacy 静态密钥路径)。
/// 返回是否成功建立"经过验签的"会话。
/// </summary>
public static bool ApplyHandshake(HandshakeResponse resp)
{
if (resp == null || !resp.success)
{
return false;
}
// 无令牌(旧服务端):什么都不做,维持 legacy 行为。
if (string.IsNullOrWhiteSpace(resp.session_token))
{
return false;
}
// 若服务端提供了签名且客户端内置了公钥,则必须验签通过才采用会话密钥。
bool signatureRequired = !string.IsNullOrWhiteSpace(ServerPublicKeyPem)
&& !string.IsNullOrWhiteSpace(resp.server_signature);
if (signatureRequired)
{
string payload = $"{resp.steam_id}|{resp.session_token}|{resp.session_key}|{resp.expires_at}";
if (!VerifyServerSignature(payload, resp.server_signature))
{
Debug.LogWarning("[GameServerSession] 服务端签名验证失败,拒绝采用该会话密钥。");
Clear();
return false;
}
}
_sessionToken = resp.session_token ?? string.Empty;
_sessionKey = resp.session_key ?? string.Empty;
_expiresAtUtc = resp.expires_at ?? string.Empty;
return true;
}
/// <summary>
/// 计算成绩 HMAC:优先用会话密钥;无会话密钥时用传入的 legacy 静态密钥回退。
/// </summary>
public static string ComputeScoreHmac(string payload, string legacyStaticSecret)
{
string secret = !string.IsNullOrWhiteSpace(_sessionKey) ? _sessionKey : legacyStaticSecret;
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)))
{
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
var sb = new StringBuilder(hash.Length * 2);
foreach (byte b in hash) sb.Append(b.ToString("x2"));
return sb.ToString();
}
}
private static bool VerifyServerSignature(string payload, string signatureB64)
{
try
{
byte[] signature = Convert.FromBase64String(signatureB64);
RSAParameters parameters = ParsePublicKeyPem(ServerPublicKeyPem);
using (var rsa = RSA.Create())
{
// Unity 的 .NET Standard 2.0 / Mono 运行时没有 ImportFromPem
// 因此手动把 SubjectPublicKeyInfo(PEM) 解析为 RSAParameters 再导入。
rsa.ImportParameters(parameters);
return rsa.VerifyData(
Encoding.UTF8.GetBytes(payload),
signature,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
}
}
catch (Exception ex)
{
Debug.LogWarning($"[GameServerSession] 验签异常: {ex.Message}");
return false;
}
}
// ── 最小 ASN.1/DER 解析:SubjectPublicKeyInfo(PEM) -> RSAParameters ──
// 结构: SEQUENCE { SEQUENCE { OID rsaEncryption, NULL }, BIT STRING { SEQUENCE { INTEGER modulus, INTEGER exponent } } }
private static RSAParameters ParsePublicKeyPem(string pem)
{
string base64 = ExtractPemBody(pem);
byte[] der = Convert.FromBase64String(base64);
int index = 0;
ReadSequence(der, ref index); // 外层 SEQUENCE
SkipAlgorithmIdentifier(der, ref index); // 跳过 AlgorithmIdentifier SEQUENCE
// BIT STRING
ExpectTag(der, ref index, 0x03);
int bitStringLength = ReadLength(der, ref index);
if (bitStringLength < 1 || der[index] != 0x00)
{
throw new FormatException("Unexpected BIT STRING padding in public key.");
}
index += 1; // 跳过 BIT STRING 的未使用位计数(0x00)
ReadSequence(der, ref index); // RSAPublicKey SEQUENCE
byte[] modulus = ReadIntegerUnsigned(der, ref index);
byte[] exponent = ReadIntegerUnsigned(der, ref index);
return new RSAParameters { Modulus = modulus, Exponent = exponent };
}
private static string ExtractPemBody(string pem)
{
var sb = new StringBuilder(pem.Length);
using (var reader = new System.IO.StringReader(pem))
{
string line;
while ((line = reader.ReadLine()) != null)
{
string trimmed = line.Trim();
if (trimmed.Length == 0 || trimmed.StartsWith("-----"))
{
continue;
}
sb.Append(trimmed);
}
}
return sb.ToString();
}
private static void ExpectTag(byte[] data, ref int index, byte tag)
{
if (index >= data.Length || data[index] != tag)
{
throw new FormatException($"Expected ASN.1 tag 0x{tag:X2} at offset {index}.");
}
index += 1;
}
private static void ReadSequence(byte[] data, ref int index)
{
ExpectTag(data, ref index, 0x30);
ReadLength(data, ref index);
}
private static void SkipAlgorithmIdentifier(byte[] data, ref int index)
{
ExpectTag(data, ref index, 0x30);
int length = ReadLength(data, ref index);
index += length; // 整段 AlgorithmIdentifier 内容不需要
}
private static int ReadLength(byte[] data, ref int index)
{
int first = data[index];
index += 1;
if ((first & 0x80) == 0)
{
return first; // 短格式
}
int byteCount = first & 0x7F;
if (byteCount == 0 || byteCount > 4)
{
throw new FormatException("Unsupported ASN.1 length encoding.");
}
int length = 0;
for (int i = 0; i < byteCount; i++)
{
length = (length << 8) | data[index];
index += 1;
}
return length;
}
private static byte[] ReadIntegerUnsigned(byte[] data, ref int index)
{
ExpectTag(data, ref index, 0x02);
int length = ReadLength(data, ref index);
int start = index;
index += length;
// 去掉 DER 正整数为避免歧义而添加的前导 0x00 符号字节
while (length > 1 && data[start] == 0x00)
{
start += 1;
length -= 1;
}
byte[] result = new byte[length];
Array.Copy(data, start, result, 0, length);
return result;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 79cbd031dc2447d4789e769442806868
@@ -49,6 +49,15 @@ namespace GameServer.Client
[JsonProperty("steam_id")] public string steam_id;
[JsonProperty("error_code")] public string error_code;
[JsonProperty("message")] public string message;
// 会话令牌:后续请求以 Authorization: Bearer 携带,服务端据此绑定 steam_id。
[JsonProperty("session_token")] public string session_token;
// 每会话一次性签名密钥:客户端用它对成绩做 HMAC,取代内置静态密钥。
[JsonProperty("session_key")] public string session_key;
// 会话过期时间(UTC ISO8601)。
[JsonProperty("expires_at")] public string expires_at;
// 服务端 RSA 签名(base64),客户端用内置公钥验签以确认会话密钥来自真实服务端。
[JsonProperty("server_signature")] public string server_signature;
[JsonProperty("sign_alg")] public string sign_alg;
}
[Serializable]
@@ -9,11 +9,7 @@ using UnityEngine;
using UnityEngine.Networking;
using Bansonic;
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
#define NETWORK_DISABLE_STEAMWORKS
#endif
#if !NETWORK_DISABLE_STEAMWORKS
#if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX
using Steamworks;
#endif
@@ -32,7 +28,7 @@ public class NetworkManager : MonoBehaviour
public const string SteamAvatarUrlPrefix = "steam-avatar://";
[Header("Server")]
[SerializeField] private string serverUrl = "http://47.112.187.172:8080";
[SerializeField] private string serverUrl = "https://game.bansonic.top";
[Header("Auth")]
public string steamId = "";
@@ -820,6 +816,13 @@ public class NetworkManager : MonoBehaviour
SetState(ConnectionState.Handshaking);
HandshakeResponse resp = await PostJson<HandshakeResponse>(BuildApiUrl("/api/handshake"), req, token);
// 存储会话令牌与每会话签名密钥(先用内置 RSA 公钥验签,确认来自真实服务端)。
if (resp != null && resp.success)
{
GameServerSession.ApplyHandshake(resp);
}
OnHandshakeResult?.Invoke(resp);
if (resp != null && resp.success)
@@ -1350,7 +1353,7 @@ public class NetworkManager : MonoBehaviour
{
currentSteamId = string.Empty;
currentDisplayName = string.Empty;
#if NETWORK_DISABLE_STEAMWORKS
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
if (logWarnings)
{
Debug.LogWarning("[NetworkManager] Steamworks is unavailable on this platform. Using current serialized network identity.");
@@ -1396,7 +1399,7 @@ public class NetworkManager : MonoBehaviour
private static bool TryReadSteamPersona(string targetSteamId, out string displayName)
{
displayName = string.Empty;
#if NETWORK_DISABLE_STEAMWORKS
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
return false;
#else
if (string.IsNullOrWhiteSpace(targetSteamId) || !SteamManager.Initialized || !ulong.TryParse(targetSteamId, out ulong parsedSteamId))
@@ -1426,7 +1429,7 @@ public class NetworkManager : MonoBehaviour
private async Task<byte[]> GetLocalSteamAvatarPngAsync()
{
#if NETWORK_DISABLE_STEAMWORKS
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
return null;
#else
if (!SteamManager.Initialized)
@@ -1532,6 +1535,20 @@ public class NetworkManager : MonoBehaviour
}
}
private static void ApplyAuthHeader(UnityWebRequest request)
{
if (request == null)
{
return;
}
string sessionToken = GameServerSession.SessionToken;
if (!string.IsNullOrWhiteSpace(sessionToken))
{
request.SetRequestHeader("Authorization", "Bearer " + sessionToken);
}
}
private async Task<T> PostJson<T>(string url, object payload, CancellationToken token)
{
if (OnlineModeSettings.IsLocalOnlyMode)
@@ -1546,6 +1563,7 @@ public class NetworkManager : MonoBehaviour
request.uploadHandler = new UploadHandlerRaw(body);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
ApplyAuthHeader(request);
request.timeout = 15;
if (VerboseLogs) Debug.Log($"[NetworkManager] HTTP POST {url}");
@@ -1582,6 +1600,7 @@ public class NetworkManager : MonoBehaviour
request.uploadHandler = new UploadHandlerRaw(body);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
ApplyAuthHeader(request);
request.timeout = 15;
if (VerboseLogs) Debug.Log($"[NetworkManager] HTTP PUT {url}");
@@ -1613,6 +1632,7 @@ public class NetworkManager : MonoBehaviour
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
ApplyAuthHeader(request);
request.timeout = 15;
if (VerboseLogs) Debug.Log($"[NetworkManager] HTTP GET {url}");
await SendRequestAsync(request.SendWebRequest(), token);