From 46a3443c6f1e9ebe3f51714e8e2b750a567682e9 Mon Sep 17 00:00:00 2001 From: BlackSpirits Date: Sat, 12 Sep 2026 17:29:43 +0200 Subject: [PATCH 1/2] Verify FFmpeg shared-library downloads --- .../Download/FfmpegLibsDownloadService.cs | 61 +++++++++++++++++-- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/src/ui/Logic/Download/FfmpegLibsDownloadService.cs b/src/ui/Logic/Download/FfmpegLibsDownloadService.cs index d4ebb23c7c5..f970826fec6 100644 --- a/src/ui/Logic/Download/FfmpegLibsDownloadService.cs +++ b/src/ui/Logic/Download/FfmpegLibsDownloadService.cs @@ -1,4 +1,5 @@ using Nikse.SubtitleEdit.Logic.VideoPlayers.Ffmpeg; +using Nikse.SubtitleEdit.UiLogic; using System; using System.IO; using System.Net.Http; @@ -21,21 +22,71 @@ public interface IFfmpegLibsDownloadService /// public class FfmpegLibsDownloadService(HttpClient httpClient) : IFfmpegLibsDownloadService { - private static readonly string WindowsX64Url = - $"https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n{FfmpegLibraries.MajorVersion}-latest-win64-lgpl-shared-{FfmpegLibraries.MajorVersion}.zip"; + // Pin one dated autobuild rather than BtbN's mutable "latest" alias. A future FFmpeg update + // must deliberately update both this URL and its GitHub-published SHA-256 together. + internal const string WindowsX64Url = + "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-09-11-13-20/ffmpeg-n9.0.1-29-gad500d59cb-win64-lgpl-shared-9.0.zip"; + internal const string WindowsX64Sha256 = + "40eec25b2f55dcad7e4d4e640919b920d29818b56fdaf9353ce1fd8adefc9d6b"; public async Task DownloadFfmpegLibs(string destinationFileName, IProgress? progress, CancellationToken cancellationToken) { - await DownloadHelper.DownloadFileAsync(httpClient, GetUrl(), destinationFileName, progress, cancellationToken); + var download = GetDownload(); + await DownloadAndVerifyAsync(httpClient, download.Url, download.Sha256, destinationFileName, progress, cancellationToken); } - private static string GetUrl() + internal static async Task DownloadAndVerifyAsync( + HttpClient client, + string url, + string expectedSha256, + string destinationFileName, + IProgress? progress, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(expectedSha256)) + { + throw new InvalidOperationException("No SHA-256 is registered for the FFmpeg shared-library archive."); + } + + try + { + await DownloadHelper.DownloadFileAsync(client, url, destinationFileName, progress, cancellationToken); + var actual = await Sha256Util.ComputeSha256Async(destinationFileName, cancellationToken); + if (string.IsNullOrEmpty(actual) || !string.Equals(expectedSha256, actual, StringComparison.OrdinalIgnoreCase)) + { + throw new IOException( + $"FFmpeg shared-library download failed integrity check (expected SHA-256 {expectedSha256}, got {actual ?? ""})."); + } + } + catch + { + TryDelete(destinationFileName); + throw; + } + } + + private static (string Url, string Sha256) GetDownload() { if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.X64) { - return WindowsX64Url; + return (WindowsX64Url, WindowsX64Sha256); } throw new PlatformNotSupportedException("FFmpeg shared library download is only available for Windows x64; install FFmpeg from your package manager instead."); } + + private static void TryDelete(string fileName) + { + try + { + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + } + catch + { + // Best effort: preserve the original download/integrity failure. + } + } } From 3c39165f12ec36cdf53eafb5f418f40e6f302ce9 Mon Sep 17 00:00:00 2001 From: BlackSpirits Date: Sat, 12 Sep 2026 17:29:59 +0200 Subject: [PATCH 2/2] Add FFmpeg shared-library integrity regressions --- .../FfmpegLibsDownloadServiceTests.cs | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 tests/UI/Logic/Download/FfmpegLibsDownloadServiceTests.cs diff --git a/tests/UI/Logic/Download/FfmpegLibsDownloadServiceTests.cs b/tests/UI/Logic/Download/FfmpegLibsDownloadServiceTests.cs new file mode 100644 index 00000000000..0b602b2f4fc --- /dev/null +++ b/tests/UI/Logic/Download/FfmpegLibsDownloadServiceTests.cs @@ -0,0 +1,115 @@ +using System.Net; +using System.Text; +using Nikse.SubtitleEdit.Logic.Download; + +namespace UITests.Logic.Download; + +public class FfmpegLibsDownloadServiceTests +{ + [Fact] + public void PinnedAsset_MatchesPublishedReleaseDigest() + { + Assert.Equal( + "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-09-11-13-20/ffmpeg-n9.0.1-29-gad500d59cb-win64-lgpl-shared-9.0.zip", + FfmpegLibsDownloadService.WindowsX64Url); + Assert.Equal( + "40eec25b2f55dcad7e4d4e640919b920d29818b56fdaf9353ce1fd8adefc9d6b", + FfmpegLibsDownloadService.WindowsX64Sha256); + Assert.False(FfmpegLibsDownloadService.WindowsX64Url.Contains("/latest/", StringComparison.Ordinal)); + } + + [Fact] + public async Task DownloadAndVerifyAsync_TamperedPayload_RejectsAndDeletesFile() + { + var handler = new StaticResponseHandler(Encoding.ASCII.GetBytes("tampered")); + using var httpClient = new HttpClient(handler); + var destination = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".zip"); + + try + { + await Assert.ThrowsAsync(() => + FfmpegLibsDownloadService.DownloadAndVerifyAsync( + httpClient, + FfmpegLibsDownloadService.WindowsX64Url, + FfmpegLibsDownloadService.WindowsX64Sha256, + destination, + progress: null, + TestContext.Current.CancellationToken)); + + Assert.False(File.Exists(destination)); + Assert.Equal(new[] { HttpMethod.Head, HttpMethod.Get }, handler.RequestMethods); + } + finally + { + if (File.Exists(destination)) + { + File.Delete(destination); + } + } + } + + [Fact] + public async Task DownloadAndVerifyAsync_ValidPayload_PreservesFile() + { + var payload = Encoding.ASCII.GetBytes("abc"); + var handler = new StaticResponseHandler(payload); + using var httpClient = new HttpClient(handler); + var destination = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".zip"); + + try + { + await FfmpegLibsDownloadService.DownloadAndVerifyAsync( + httpClient, + "https://example.invalid/ffmpeg-test.zip", + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + destination, + progress: null, + TestContext.Current.CancellationToken); + + Assert.True(File.Exists(destination)); + Assert.Equal(payload, await File.ReadAllBytesAsync(destination, TestContext.Current.CancellationToken)); + Assert.Equal(new[] { HttpMethod.Head, HttpMethod.Get }, handler.RequestMethods); + } + finally + { + if (File.Exists(destination)) + { + File.Delete(destination); + } + } + } + + [Fact] + public async Task DownloadAndVerifyAsync_MissingDigest_FailsClosedBeforeRequest() + { + var handler = new StaticResponseHandler(Encoding.ASCII.GetBytes("unused")); + using var httpClient = new HttpClient(handler); + var destination = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".zip"); + + await Assert.ThrowsAsync(() => + FfmpegLibsDownloadService.DownloadAndVerifyAsync( + httpClient, + "https://example.invalid/ffmpeg-future.zip", + string.Empty, + destination, + progress: null, + TestContext.Current.CancellationToken)); + + Assert.Empty(handler.RequestMethods); + Assert.False(File.Exists(destination)); + } + + private sealed class StaticResponseHandler(byte[] payload) : HttpMessageHandler + { + public List RequestMethods { get; } = new(); + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + RequestMethods.Add(request.Method); + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent(payload), + }); + } + } +}