Skip to content

feat: verify release asset integrity with SHA256 checksums#38

Merged
hieuck merged 1 commit into
mainfrom
feature/verify-asset-hash
Jul 8, 2026
Merged

feat: verify release asset integrity with SHA256 checksums#38
hieuck merged 1 commit into
mainfrom
feature/verify-asset-hash

Conversation

@hieuck

@hieuck hieuck commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Summary\r\nAdd SHA256 checksum verification for downloaded update assets to mitigate supply-chain risks.\r\n\r\n## Changes\r\n- Add helper to compute/parse/verify SHA256 hashes.\r\n- now includes a pointing to .\r\n- downloads and verifies the asset hash before installation.\r\n- If verification fails, the update is aborted and the user is notified.\r\n- CI generates for each release and uploads it as a release asset.\r\n\r\n## Tests\r\n- Added 7 unit tests for .\r\n- MSBUILD : error MSB1003: Specify a project or solution file. The current working directory does not contain a project or solution file. passes: 60/60.\r\n\r\nCloses #34.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@hieuck, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 42 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2b387518-34ce-44b5-af3a-c748063e42c9

📥 Commits

Reviewing files that changed from the base of the PR and between 88d8e98 and c436e5f.

📒 Files selected for processing (7)
  • .github/workflows/release.yml
  • src/AssetVerifier.cs
  • src/KeePassAutoReload.csproj
  • src/KeePassAutoReloadExt.cs
  • src/UpdateChecker.cs
  • tests/KeePassAutoReload.Tests.csproj
  • tests/Tests.cs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/verify-asset-hash

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/AssetVerifier.cs
Comment on lines +15 to +20
using (FileStream stream = File.OpenRead(filePath))
using (SHA256 sha256 = SHA256.Create())
{
byte[] hash = sha256.ComputeHash(stream);
return BytesToHex(hash);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ComputeSha256 method 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:

try {
    using (FileStream stream = File.OpenRead(filePath))
    using (SHA256 sha256 = SHA256.Create())
    {
        byte[] hash = sha256.ComputeHash(stream);
        return BytesToHex(hash);
    }
} catch (IOException ex) {
    // Handle or log the exception as appropriate
    throw new InvalidOperationException($"Failed to compute hash for '{filePath}'", ex);
}

Comment thread src/AssetVerifier.cs
Comment on lines +36 to +40
int separatorIndex = trimmed.IndexOf(' ');
if (separatorIndex <= 0) continue;

string hash = trimmed.Substring(0, separatorIndex).Trim();
string fileName = trimmed.Substring(separatorIndex + 1).Trim();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parsing logic in ParseChecksums splits each line on the first space character, which assumes that file names do not contain spaces. If a file name contains spaces, this will result in incorrect parsing and potentially invalid hash-to-file mappings.

Recommendation:
Consider using a more robust parsing strategy, such as splitting on the last space or using a delimiter that cannot appear in file names. Alternatively, document the expected format and enforce it during checksum file generation.

Comment on lines 179 to 215
{
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
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 File.Replace or equivalent) and ensure all file operations are robust against interruptions. Additionally, validate all file paths before performing operations to prevent potential path traversal or unintended file overwrites.

Comment on lines 256 to 272
}
}

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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method GetPluginPackagePath() does not validate the returned path, which could lead to security issues if the path is outside the intended plugin directory. Ensure that the resolved path is within the expected directory and does not allow for path traversal or writing to unintended locations. This is especially important when performing update operations that overwrite executable code.

Comment thread src/UpdateChecker.cs
Comment on lines 116 to 122
info.LatestVersion = tagName;
info.ReleaseUrl = BuildReleaseUrl(tagName);
info.AssetUrl = BuildAssetUrl(tagName, format);
info.ChecksumUrl = BuildChecksumUrl(tagName);
info.IsUpdateAvailable = IsNewerVersion(GetCurrentVersion(), tagName);
return info;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code constructs the UpdateInfo object without validating whether tagName is null or empty. If the GitHub API response is malformed or contains no valid tags, tagName will be empty, resulting in URLs that point to the generic releases page and potentially setting IsUpdateAvailable incorrectly. This could mislead users or cause downstream errors.

Recommended solution:
Add a check after extracting tagName to ensure it is not null or empty. If it is, either throw an exception or return an UpdateInfo object indicating that update information could not be determined.

if (string.IsNullOrEmpty(tagName))
    throw new InvalidOperationException("No valid release tag found in the API response.");

- Add AssetVerifier helper to compute and verify SHA256 hashes.

- Add ChecksumUrl to UpdateInfo and download SHA256SUMS.txt during updates.

- Verify downloaded asset hash before replacing the active plugin file.

- Update CI to generate SHA256SUMS.txt and attach it to releases.

- Add 7 unit tests for AssetVerifier; dotnet test passes 60/60.

Closes #34.
@hieuck hieuck force-pushed the feature/verify-asset-hash branch from 8b7ee9b to c436e5f Compare July 8, 2026 01:50
Comment thread src/UpdateChecker.cs
Comment on lines 193 to 194
{
List<string> values = new List<string>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fragile JSON Parsing with Regex

The ExtractJsonStrings method uses a regular expression to extract values from JSON. Parsing JSON with regex is error-prone and fragile, especially if the JSON structure changes, contains nested objects, or includes escaped characters. This can lead to incorrect extraction or missed values, impacting the reliability of update checks.

Recommended solution:
Use a proper JSON parser such as System.Text.Json or Newtonsoft.Json to extract values robustly:

using System.Text.Json;
...
var doc = JsonDocument.Parse(json);
// Extract values using JsonElement traversal

This approach is more maintainable and less error-prone.

@hieuck hieuck merged commit 50feefc into main Jul 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant