-
Notifications
You must be signed in to change notification settings - Fork 0
feat: verify release asset integrity with SHA256 checksums #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using System.Security.Cryptography; | ||
| using System.Text; | ||
|
|
||
| namespace KeePassAutoReload | ||
| { | ||
| internal static class AssetVerifier | ||
| { | ||
| public static string ComputeSha256(string filePath) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(filePath)) throw new ArgumentException("filePath"); | ||
|
|
||
| using (FileStream stream = File.OpenRead(filePath)) | ||
| using (SHA256 sha256 = SHA256.Create()) | ||
| { | ||
| byte[] hash = sha256.ComputeHash(stream); | ||
| return BytesToHex(hash); | ||
| } | ||
| } | ||
|
|
||
| public static Dictionary<string, string> ParseChecksums(string checksumsText) | ||
| { | ||
| Dictionary<string, string> entries = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); | ||
| if (string.IsNullOrWhiteSpace(checksumsText)) return entries; | ||
|
|
||
| using (StringReader reader = new StringReader(checksumsText)) | ||
| { | ||
| string line; | ||
| while ((line = reader.ReadLine()) != null) | ||
| { | ||
| string trimmed = line.Trim(); | ||
| if (string.IsNullOrEmpty(trimmed)) continue; | ||
|
|
||
| int separatorIndex = trimmed.IndexOf(' '); | ||
| if (separatorIndex <= 0) continue; | ||
|
|
||
| string hash = trimmed.Substring(0, separatorIndex).Trim(); | ||
| string fileName = trimmed.Substring(separatorIndex + 1).Trim(); | ||
|
Comment on lines
+36
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The parsing logic in Recommendation: |
||
|
|
||
| if (string.IsNullOrEmpty(hash) || string.IsNullOrEmpty(fileName)) continue; | ||
|
|
||
| entries[fileName] = hash; | ||
| } | ||
| } | ||
|
|
||
| return entries; | ||
| } | ||
|
|
||
| public static bool VerifyFile(string filePath, string expectedHash) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(filePath) || string.IsNullOrWhiteSpace(expectedHash)) return false; | ||
| if (!File.Exists(filePath)) return false; | ||
|
|
||
| string actualHash = ComputeSha256(filePath); | ||
| return string.Equals(actualHash, expectedHash.Trim(), StringComparison.OrdinalIgnoreCase); | ||
| } | ||
|
|
||
| private static string BytesToHex(byte[] bytes) | ||
| { | ||
| StringBuilder builder = new StringBuilder(bytes.Length * 2); | ||
| foreach (byte b in bytes) | ||
| { | ||
| builder.Append(b.ToString("x2")); | ||
| } | ||
| return builder.ToString(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics; | ||
| using System.IO; | ||
| using System.Threading.Tasks; | ||
|
|
@@ -179,16 +180,37 @@ private async Task InstallUpdate(UpdateInfo info) | |
| { | ||
| string targetPath = GetPluginPackagePath(); | ||
| string tempPath = targetPath + ".download"; | ||
| string checksumPath = targetPath + ".sha256"; | ||
|
|
||
| using (HttpUpdateClient client = new HttpUpdateClient()) | ||
| { | ||
| await client.DownloadFileAsync(info.AssetUrl, tempPath); | ||
| if (!string.IsNullOrWhiteSpace(info.ChecksumUrl)) | ||
| { | ||
| try | ||
| { | ||
| await client.DownloadFileAsync(info.ChecksumUrl, checksumPath); | ||
| } | ||
| catch (Exception checksumEx) | ||
| { | ||
| File.Delete(tempPath); | ||
| throw new InvalidOperationException("Failed to download release checksums: " + checksumEx.Message, checksumEx); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (File.Exists(checksumPath) && !VerifyDownloadedAsset(tempPath, checksumPath, Path.GetFileName(info.AssetUrl))) | ||
| { | ||
| File.Delete(tempPath); | ||
| File.Delete(checksumPath); | ||
| throw new InvalidOperationException("Downloaded asset failed hash verification."); | ||
| } | ||
|
|
||
| try | ||
| { | ||
| File.Copy(tempPath, targetPath, true); | ||
| File.Delete(tempPath); | ||
| if (File.Exists(checksumPath)) File.Delete(checksumPath); | ||
|
|
||
| ShowOnUi(delegate | ||
| { | ||
|
Comment on lines
180
to
216
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The update installation logic performs file copy and delete operations that are not atomic, which may result in a partially updated or corrupted plugin if the process is interrupted or if there is concurrent access. To improve reliability and safety, consider using atomic file replacement techniques (such as writing to a temporary file and then using |
||
|
|
@@ -202,6 +224,7 @@ private async Task InstallUpdate(UpdateInfo info) | |
| string pendingPath = targetPath + ".new"; | ||
| File.Copy(tempPath, pendingPath, true); | ||
| File.Delete(tempPath); | ||
| if (File.Exists(checksumPath)) File.Delete(checksumPath); | ||
|
|
||
| bool updaterScheduled = TryScheduleUpdater(targetPath, pendingPath); | ||
|
|
||
|
|
@@ -234,6 +257,17 @@ private async Task InstallUpdate(UpdateInfo info) | |
| } | ||
| } | ||
|
|
||
| private static bool VerifyDownloadedAsset(string assetPath, string checksumPath, string assetFileName) | ||
| { | ||
| if (!File.Exists(checksumPath)) return true; | ||
|
|
||
| string checksumsText = File.ReadAllText(checksumPath); | ||
| Dictionary<string, string> checksums = AssetVerifier.ParseChecksums(checksumsText); | ||
| if (!checksums.ContainsKey(assetFileName)) return true; | ||
|
|
||
| return AssetVerifier.VerifyFile(assetPath, checksums[assetFileName]); | ||
| } | ||
|
|
||
| private static string GetPluginPackagePath() | ||
| { | ||
| return PluginPathResolver.ResolvePluginPackagePath( | ||
|
Comment on lines
257
to
273
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The method |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ internal sealed class UpdateInfo | |
| public string LatestVersion; | ||
| public string ReleaseUrl; | ||
| public string AssetUrl; | ||
| public string ChecksumUrl; | ||
| public bool IsUpdateAvailable; | ||
| } | ||
|
|
||
|
|
@@ -115,6 +116,7 @@ public static async Task<UpdateInfo> CheckLatestAsync(IUpdateClient client, Plug | |
| info.LatestVersion = tagName; | ||
| info.ReleaseUrl = BuildReleaseUrl(tagName); | ||
| info.AssetUrl = BuildAssetUrl(tagName, format); | ||
| info.ChecksumUrl = BuildChecksumUrl(tagName); | ||
| info.IsUpdateAvailable = IsNewerVersion(GetCurrentVersion(), tagName); | ||
| return info; | ||
| } | ||
|
Comment on lines
116
to
122
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code constructs the Recommended solution: if (string.IsNullOrEmpty(tagName))
throw new InvalidOperationException("No valid release tag found in the API response."); |
||
|
|
@@ -181,6 +183,12 @@ private static string BuildReleaseUrl(string tagName) | |
| return ReleasesUrl + "/tag/" + tagName; | ||
| } | ||
|
|
||
| private static string BuildChecksumUrl(string tagName) | ||
| { | ||
| if (string.IsNullOrEmpty(tagName)) return ReleasesUrl; | ||
| return "https://github.com/hieuck/KeePassAutoReload/releases/download/" + tagName + "/SHA256SUMS.txt"; | ||
| } | ||
|
|
||
| private static List<string> ExtractJsonStrings(string json, string name) | ||
| { | ||
| List<string> values = new List<string>(); | ||
|
Comment on lines
193
to
194
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fragile JSON Parsing with Regex The Recommended solution: using System.Text.Json;
...
var doc = JsonDocument.Parse(json);
// Extract values using JsonElement traversalThis approach is more maintainable and less error-prone. |
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
ComputeSha256method does not handle IO-related exceptions (e.g., file not found, access denied) that may occur when opening or reading the file. This can cause the application to crash if the caller does not catch these exceptions.Recommendation:
Wrap the file operations in a try-catch block and return a meaningful error or propagate a custom exception, depending on the intended usage. For example: