using System; using System.Globalization; using System.Security.Cryptography; using System.Text; namespace Jellyfin.Plugin.Chaosflix.Api; /// /// Signs the proxy URLs the channel hands out, so the endpoint only serves /// recordings this plugin selected. Without a signature anyone who can reach /// the server could stream arbitrary CCC recordings through it. /// public static class ProxySignature { /// Query parameter carrying the signature. public const string QueryParameter = "t"; /// /// Secret for this server process. It is deliberately not persisted: media /// sources are handed out per playback, so a restart simply means the next /// request gets a freshly signed url, and nothing has to be stored or /// migrated. /// public static string Secret { get; } = Guid.NewGuid().ToString("N"); /// /// Signs one recording. The signature covers exactly the parameters the /// controller acts on, so none of them can be swapped afterwards. /// public static string Create(string eventGuid, string? recordingFolder, string? language, string? filename = null) { var payload = string.Create( CultureInfo.InvariantCulture, $"{eventGuid}\n{recordingFolder ?? string.Empty}\n{language ?? string.Empty}\n{filename ?? string.Empty}"); var hash = HMACSHA256.HashData(Encoding.UTF8.GetBytes(Secret), Encoding.UTF8.GetBytes(payload)); return Convert.ToHexStringLower(hash); } /// /// Verifies a signature in constant time. /// public static bool Verify(string eventGuid, string? recordingFolder, string? language, string? signature, string? filename = null) { if (string.IsNullOrEmpty(signature)) { return false; } var expected = Create(eventGuid, recordingFolder, language, filename); return CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(signature)); } }