using System.Collections.Concurrent; using System.Security.Cryptography; using WebDrop.Core; namespace WebDrop.Api; public sealed record PasskeyCeremony(string Username, string Purpose, string State, string PrfSalt, string? SessionHash, DateTimeOffset Expires); public sealed class PasskeyCeremonies { private readonly ConcurrentDictionary _pending = new(); public string Put(PasskeyCeremony ceremony) { foreach (var entry in _pending) if (entry.Value.Expires <= DateTimeOffset.UtcNow) _pending.TryRemove(entry.Key, out _); if (_pending.Count >= 1000) throw new WebDropException(ErrorCode.RateLimited); var id = Codec.Encode(RandomNumberGenerator.GetBytes(32)); _pending[id] = ceremony; return id; } public PasskeyCeremony Take(string id, string username, string purpose, string? token) { if (string.IsNullOrEmpty(id) || id.Length > 100 || !_pending.TryRemove(id, out var ceremony) || ceremony.Expires <= DateTimeOffset.UtcNow || ceremony.Username != username || ceremony.Purpose != purpose || ceremony.SessionHash != Hash(token)) throw new WebDropException(ErrorCode.BadRequest); return ceremony; } public static string? Hash(string? token) => token is null ? null : Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(token))); }