chip-game/Assets/Scripts/Server/AuthManager.cs
_sega ee79e312f1 FEAT Получение пароля из файла
Теперь пароль от сайт-панели корректно берется из persistantDataPath.
Файл admin_password.txt
2026-02-02 11:05:48 +03:00

196 lines
6.3 KiB
C#

using System;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
public static class AuthManager
{
private const string CookieName = "fishka_auth";
private const string TokenSeparator = ".";
private const string AppSalt = "FISHKA_WEB_ADMIN_V1"; // поменяй на своё
// Берем пароль из файла (если есть), иначе fallback из инспектора
public static string LoadPasswordOrFallback(string fallback, string fileName)
{
try
{
string path = Path.Combine(Application.persistentDataPath, fileName);
if (File.Exists(path))
{
var s = File.ReadAllText(path, Encoding.UTF8).Trim();
if (!string.IsNullOrEmpty(s))
{
Debug.Log($"Password loaded from file. {s}");
return s;
}
}
}
catch (Exception e)
{
Debug.LogWarning($"Password file read failed: {e.Message}");
}
return fallback ?? "";
}
public static bool VerifyPassword(string entered, string actual)
{
if (entered == null) entered = "";
if (actual == null) actual = "";
return ConstantTimeEquals(entered, actual);
}
public static bool IsAuthorized(HttpListenerRequest request, string password)
{
try
{
var cookie = request.Cookies?[CookieName];
if (cookie == null || string.IsNullOrEmpty(cookie.Value)) return false;
return ValidateToken(cookie.Value, password);
}
catch
{
return false;
}
}
public static void SetAuthCookie(HttpListenerResponse response, string token, int daysValid)
{
// Cookie через header руками, чтобы не упираться в SameSite API разных .NET в Unity
int maxAge = Mathf.Clamp(daysValid, 1, 3650) * 24 * 60 * 60;
string header = $"{CookieName}={token}; Path=/; Max-Age={maxAge}; HttpOnly; SameSite=Lax";
response.Headers.Add("Set-Cookie", header);
}
public static string GenerateToken(string password, int daysValid)
{
long exp = DateTimeOffset.UtcNow.AddDays(daysValid).ToUnixTimeSeconds();
string nonce = Guid.NewGuid().ToString("N");
string payload = $"{exp}:{nonce}";
byte[] sig = HmacSha256(GetSecretKey(password), payload);
return Base64UrlEncode(Encoding.UTF8.GetBytes(payload)) + TokenSeparator + Base64UrlEncode(sig);
}
private static bool ValidateToken(string token, string password)
{
if (string.IsNullOrEmpty(token)) return false;
var parts = token.Split(new[] { TokenSeparator }, StringSplitOptions.None);
if (parts.Length != 2) return false;
byte[] payloadBytes, sigBytes;
try
{
payloadBytes = Base64UrlDecode(parts[0]);
sigBytes = Base64UrlDecode(parts[1]);
}
catch { return false; }
string payload = Encoding.UTF8.GetString(payloadBytes);
byte[] expected = HmacSha256(GetSecretKey(password), payload);
if (!FixedTimeEquals(sigBytes, expected)) return false;
// exp
var idx = payload.IndexOf(':');
if (idx <= 0) return false;
if (!long.TryParse(payload.Substring(0, idx), out long exp)) return false;
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (now > exp) return false;
return true;
}
private static byte[] GetSecretKey(string password)
{
// Если поменяешь пароль — старые куки станут невалидными (это плюс).
string s = $"{AppSalt}:{password}";
return Sha256(Encoding.UTF8.GetBytes(s));
}
private static byte[] Sha256(byte[] data)
{
using var sha = SHA256.Create();
return sha.ComputeHash(data);
}
private static byte[] HmacSha256(byte[] key, string data)
{
using var h = new HMACSHA256(key);
return h.ComputeHash(Encoding.UTF8.GetBytes(data));
}
// Достаём пароль из body: либо form-url-encoded (password=...), либо json {"password":"..."}
public static string ExtractPassword(string body, string contentType)
{
if (string.IsNullOrEmpty(body)) return "";
contentType = (contentType ?? "").ToLower();
if (contentType.Contains("application/json"))
{
// очень простой парсер под {"password":"..."}
// если хочешь — можно заменить на DTO+JsonUtility
const string key = "\"password\"";
int i = body.IndexOf(key, StringComparison.OrdinalIgnoreCase);
if (i < 0) return "";
int colon = body.IndexOf(':', i);
if (colon < 0) return "";
int q1 = body.IndexOf('"', colon + 1);
if (q1 < 0) return "";
int q2 = body.IndexOf('"', q1 + 1);
if (q2 < 0) return "";
return body.Substring(q1 + 1, q2 - q1 - 1);
}
// form
try
{
var nvc = System.Web.HttpUtility.ParseQueryString(body, Encoding.UTF8);
return nvc["password"] ?? "";
}
catch
{
return "";
}
}
private static string Base64UrlEncode(byte[] data)
{
return Convert.ToBase64String(data)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
private static byte[] Base64UrlDecode(string s)
{
s = s.Replace('-', '+').Replace('_', '/');
switch (s.Length % 4)
{
case 2: s += "=="; break;
case 3: s += "="; break;
}
return Convert.FromBase64String(s);
}
private static bool ConstantTimeEquals(string a, string b)
{
var ba = Encoding.UTF8.GetBytes(a);
var bb = Encoding.UTF8.GetBytes(b);
return FixedTimeEquals(ba, bb);
}
private static bool FixedTimeEquals(byte[] a, byte[] b)
{
if (a == null || b == null) return false;
if (a.Length != b.Length) return false;
int diff = 0;
for (int i = 0; i < a.Length; i++)
diff |= a[i] ^ b[i];
return diff == 0;
}
}