using System.Security.Cryptography; using System.Text; namespace MusicBridge.Agent.Network; internal sealed record PairingWindow(string Code, DateTimeOffset ExpiresAt); internal sealed record PairingResult(bool Success, string? Token, string Code, string? DeviceId = null); internal sealed class PairingService(TimeProvider? clock = null, IdentityStore? store = null) { private readonly TimeProvider clock = clock ?? TimeProvider.System; private readonly object gate = new(); private TrustedDevice[] devices = store?.Devices.ToArray() ?? []; public event Action? TrustChanged; public IReadOnlyList Devices { get { lock (gate) return devices.ToArray(); } } private PairingWindow? window; private int attempts; public PairingWindow Open() { lock (gate) { attempts = 0; return window = new(RandomNumberGenerator.GetInt32(100_000_000).ToString("D8"), clock.GetUtcNow().AddMinutes(5)); } } public PairingWindow? Current { get { lock (gate) return window is not null && window.ExpiresAt > clock.GetUtcNow() && attempts < 10 ? window : null; } } public PairingResult Pair(string? code, string? deviceName = null) { lock (gate) { if (window is null || window.ExpiresAt <= clock.GetUtcNow() || attempts >= 10) return new(false, null, "pairing_closed"); attempts++; if (code is null || code.Length != 8 || !CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(code), Encoding.UTF8.GetBytes(window.Code))) return new(false, null, "invalid_code"); if (deviceName is not null && (string.IsNullOrWhiteSpace(deviceName) || deviceName.Length > 64 || deviceName.Any(char.IsControl))) return new(false, null, "invalid_name"); if (devices.Length >= 8) return new(false, null, "device_limit"); var token = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)); var device = new TrustedDevice(Guid.NewGuid().ToString("N"), deviceName?.Trim() ?? "iPhone", Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token))), clock.GetUtcNow()); var updated = devices.Append(device).ToArray(); try { store?.SaveDevices(updated); } catch (Exception) { return new(false, null, "storage_failed"); } devices = updated; window = null; // One successful pairing per locally opened window. return new(true, token, "ok", device.Id); } } public bool Authorize(string? token) { if (token is null || token.Length != 64) return false; var hash = SHA256.HashData(Encoding.UTF8.GetBytes(token)); lock (gate) return devices.Any(candidate => CryptographicOperations.FixedTimeEquals(Convert.FromHexString(candidate.TokenHash), hash)); } public bool Revoke(string id) { lock (gate) { var updated = devices.Where(d => d.Id != id).ToArray(); if (updated.Length == devices.Length) return false; store?.SaveDevices(updated); devices = updated; } TrustChanged?.Invoke(); return true; } }