Теперь пароль от сайт-панели корректно берется из persistantDataPath. Файл admin_password.txt
196 lines
6.3 KiB
C#
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;
|
|
}
|
|
}
|