{"record":{"id":"8163375916439c4b","repo":"lostindark/DriverStoreExplorer","slug":"sha256-hash-of-the-downloaded-file-does-not-match","errorCode":null,"errorMessage":"SHA256 hash of the downloaded file does not match the expected value.","messagePattern":"SHA256 hash of the downloaded file does not match the expected value\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"Rapr/UpdateManager.cs","lineNumber":124,"sourceCode":"            // Download the zip\n            using (var fileStream = new FileStream(tempZipPath, FileMode.Create, FileAccess.Write, FileShare.None))\n            {\n                await this.httpClient.DownloadAsync(versionInfo.DownloadUrl, fileStream, progress).ConfigureAwait(false);\n            }\n\n            // Verify SHA256 hash\n            if (!string.IsNullOrEmpty(versionInfo.Sha256))\n            {\n                using (var sha256 = SHA256.Create())\n                using (var fileStream = new FileStream(tempZipPath, FileMode.Open, FileAccess.Read, FileShare.Read))\n                {\n                    var hashBytes = sha256.ComputeHash(fileStream);\n                    var actualHash = BitConverter.ToString(hashBytes).Replace(\"-\", string.Empty);\n\n                    if (!actualHash.Equals(versionInfo.Sha256, StringComparison.OrdinalIgnoreCase))\n                    {\n                        File.Delete(tempZipPath);\n                        throw new InvalidOperationException(\"SHA256 hash of the downloaded file does not match the expected value.\");\n                    }\n                }\n            }\n\n            // Extract zip\n            ZipFile.ExtractToDirectory(tempZipPath, tempExtractPath);\n\n            // Find the actual content directory (zip may have a single root folder)\n            string sourceDir = tempExtractPath;\n            var subDirs = Directory.GetDirectories(tempExtractPath);\n            if (subDirs.Length == 1 && Directory.GetFiles(tempExtractPath).Length == 0)\n            {\n                sourceDir = subDirs[0];\n            }\n\n            string appDir = Path.GetFullPath(DSEFormHelper.GetApplicationFolder());\n            string currentExePath = Assembly.GetExecutingAssembly().Location;\n","sourceCodeStart":106,"sourceCodeEnd":142,"githubUrl":"https://github.com/lostindark/DriverStoreExplorer/blob/958fcd481bd3e212c3dcf1e531b3e332d5a8e81a/Rapr/UpdateManager.cs#L106-L142","documentation":"Integrity gate after the download completes. ApplyUpdateAsync recomputes SHA256 over the saved zip and compares it (case-insensitive, hex) to versionInfo.Sha256, which GetLatestVersionInfo parses from the GitHub asset's digest field ('sha256:<hex>'). On mismatch the temp zip is deleted and the update aborts — this is the protection against truncated downloads, MITM, or a swapped asset. The check only runs when versionInfo.Sha256 is non-empty, so a release missing the digest field silently skips verification.","triggerScenarios":"Line 113–127: bytes downloaded to tempZipPath don't hash to versionInfo.Sha256. Causes: a truncated/partial download (HttpClient.DownloadAsync dropped bytes), a proxy injecting an error page, a release whose digest was computed against a different asset, or an attacker who swapped the zip but could not forge the digest.","commonSituations":"Flaky connection leaving a short file; corporate proxy returns an HTML block page with 200; the release tag was force-pushed and asset[0] no longer matches the cached digest; the digest field format changed and the hex parsing produced a wrong value; local antivirus rewrote the downloaded file.","solutions":["Retry the update once — transient truncation is the most common cause and the next attempt often hashes clean.","Verify the published digest: compare versionInfo.Sha256 to the value shown on the GitHub release page asset (shown as a SHA-256 checksum).","Check the temp file size vs. GitHub's reported asset size: Path.Combine(Path.GetTempPath(), \"DriverStoreExplorer\") — a short file confirms truncation.","Disable any intercepting proxy or AV for the download domain and retry.","If you author releases, ensure GitHub computes and exposes the sha256 digest (asset uploaded as a binary release file yields the digest automatically)."],"exampleFix":"// before (single attempt, throws on mismatch)\nawait this.updateManager.ApplyUpdateAsync(this.latestVersionInfo, progress);\n\n// after (retry the download once before surfacing failure)\nfor (int attempt = 0; attempt < 2; attempt++)\n{\n    try\n    {\n        await this.updateManager.ApplyUpdateAsync(this.latestVersionInfo, progress);\n        break;\n    }\n    catch (InvalidOperationException ex) when (attempt == 0 && ex.Message.Contains(\"SHA256\"))\n    {\n        // Likely a truncated download; loop will re-download from scratch.\n        continue;\n    }\n}","handlingStrategy":"retry","validationCode":"// You cannot pre-validate a hash before download, but you can surface the\n// expected digest to the user and confirm the release exposes one.\nprivate static bool ReleaseExposesSha256(VersionInfo info)\n    => !string.IsNullOrWhiteSpace(info?.Sha256)\n       && info.Sha256.Length == 64\n       && System.Text.RegularExpressions.Regex.IsMatch(info.Sha256, @\"^[0-9a-fA-F]{64}$\");\n\nif (!ReleaseExposesSha256(this.latestVersionInfo))\n{\n    // Verification would be silently skipped (UpdateManager checks only when non-empty).\n    Logger.Warn(\"Release does not expose a SHA256 digest; integrity check will be bypassed.\");\n}","typeGuard":null,"tryCatchPattern":"// Retry once on a hash mismatch (most often a truncated download), then surface.\nint attempts = 0;\nwhile (true)\n{\n    try\n    {\n        await this.updateManager.ApplyUpdateAsync(this.latestVersionInfo, progress);\n        break;\n    }\n    catch (InvalidOperationException ex) when (attempts++ == 0 && ex.Message.Contains(\"SHA256\"))\n    {\n        continue;\n    }\n}","preventionTips":["Always publish releases with an exposed sha256 digest so the check is active, not skipped.","Retry a hash mismatch once before reporting failure — truncated downloads are the common cause.","Confirm the temp file size matches the GitHub asset size before hashing to catch truncation early.","Disable HTTPS-intercepting proxies for the download domain so the bytes are not rewritten.","Pin releases by tag and asset name so a re-pushed asset cannot silently change the digest your code expects."],"tags":["security","integrity","sha256","download","tamper-detection","update"],"backgroundTag":null,"analyzedSha":"958fcd481bd3e212c3dcf1e531b3e332d5a8e81a","analyzedAt":"2026-08-13T19:08:09.376Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}