using Microsoft.AspNetCore.StaticFiles; using System.Collections.Concurrent; using System.Security.Cryptography; using System.Text; namespace NotPogo.Core { public sealed class Account { public required string ScreenName { get; init; } public string ProfileId { get; init; } = "1000000000"; // pfid public string Site { get; init; } = "pogo"; public int Tokens { get; set; } = 9999; public string Avatar { get; set; } = ""; public bool OwnsPremiumAvatar { get; set; } = true; // enti.oava public bool OwnsBadges { get; set; } = true; // enti.obad } public static class Sessions { static byte[] _secret = []; // set once at startup static readonly ConcurrentDictionary Live = new(StringComparer.OrdinalIgnoreCase); public static void Init(string secret) => _secret = Encoding.UTF8.GetBytes(secret); // lkey = base64url(screenName) + "." + base64url(HMAC(screenName)) public static string Issue(string screenName) { var name = Encoding.UTF8.GetBytes(screenName); return B64(name) + "." + B64(HMACSHA256.HashData(_secret, name)); } public static Account? Find(string? lkey) { if (string.IsNullOrEmpty(lkey)) return null; var parts = lkey.Split('.'); if (parts.Length != 2) return null; try { var name = UnB64(parts[0]); if (!CryptographicOperations.FixedTimeEquals(UnB64(parts[1]), HMACSHA256.HashData(_secret, name))) return null; // forged or from another secret var screenName = Encoding.UTF8.GetString(name); // in-memory state for this run; recreated with defaults after a restart return Live.GetOrAdd(screenName, n => new Account { ScreenName = n }); } catch (FormatException) { return null; } } static string B64(byte[] b) => Convert.ToBase64String(b).TrimEnd('=').Replace('+', '-').Replace('/', '_'); static byte[] UnB64(string s) { s = s.Replace('-', '+').Replace('_', '/'); return Convert.FromBase64String(s + new string('=', (4 - s.Length % 4) % 4)); } } }