namespace Supercell.Laser.Server.SCID { using System.Collections.Concurrent; using System.Security.Cryptography; using System.Text; internal sealed class SCIDSession { public byte[] PinHash; public DateTime ExpiresAt; public int AttemptsLeft; public bool Registration; public bool Validated; } internal static class SCIDSessions { private static readonly ConcurrentDictionary Sessions = new(StringComparer.OrdinalIgnoreCase); public static string Create(string identity, bool registration) { Cleanup(); string pin = RandomNumberGenerator.GetInt32(100000, 1000000).ToString(); Sessions[identity] = new SCIDSession { PinHash = Hash(pin), ExpiresAt = DateTime.UtcNow.AddMinutes(5), AttemptsLeft = 5, Registration = registration }; return pin; } public static bool Validate(string identity, string pin, bool registration) { if (!Sessions.TryGetValue(identity, out SCIDSession session) || session.ExpiresAt <= DateTime.UtcNow || session.Registration != registration) { Sessions.TryRemove(identity, out _); return false; } lock (session) { byte[] pinHash = Hash(pin ?? string.Empty); if (session.AttemptsLeft <= 0 || !CryptographicOperations.FixedTimeEquals(session.PinHash, pinHash)) { session.AttemptsLeft--; if (session.AttemptsLeft <= 0) Sessions.TryRemove(identity, out _); return false; } session.Validated = true; return true; } } public static bool IsValidated(string identity, bool registration) { return Sessions.TryGetValue(identity, out SCIDSession session) && session.ExpiresAt > DateTime.UtcNow && session.Registration == registration && session.Validated; } public static void Remove(string identity) { Sessions.TryRemove(identity, out _); } private static void Cleanup() { DateTime now = DateTime.UtcNow; foreach (var session in Sessions) { if (session.Value.ExpiresAt <= now) Sessions.TryRemove(session.Key, out _); } } private static byte[] Hash(string value) { return SHA256.HashData(Encoding.UTF8.GetBytes(value)); } } }