From 2eec831d6a9bf2a1aecfe3c9578fb544393c5d9d Mon Sep 17 00:00:00 2001 From: J3vb <192430104+J3vb@users.noreply.github.com> Date: Sun, 19 Jul 2026 09:04:28 +0200 Subject: [PATCH] refactor(updater): export FileSHA256 and reuse it for the update snapshot (W3-2) admin's fileSHA256 duplicated VerifyChecksum's hashing body. One exported helper now serves both the TOCTOU snapshot in handleApplyUpdate and VerifyChecksum itself. Co-Authored-By: Claude Fable 5 --- Server/admin/update_handlers.go | 22 +--------------------- Server/updater/updater.go | 23 ++++++++++++++++------- 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/Server/admin/update_handlers.go b/Server/admin/update_handlers.go index 3ed3e013..65368a8b 100644 --- a/Server/admin/update_handlers.go +++ b/Server/admin/update_handlers.go @@ -2,9 +2,6 @@ package admin import ( "context" - "crypto/sha256" - "encoding/hex" - "io" "log/slog" "net/http" "os" @@ -90,7 +87,7 @@ func handleApplyUpdate(u *updater.Updater, hub HubBroadcaster, _ string) http.Ha // Snapshot the hash of the just-verified staged binary. It is re-checked // immediately before rename+spawn to close the TOCTOU window between // verification here and the swap in the background goroutine below. - stagedHash, err := fileSHA256(newPath) + stagedHash, err := updater.FileSHA256(newPath) if err != nil { slog.Error("update: failed to hash staged binary", "err", err) _ = os.Remove(newPath) @@ -159,20 +156,3 @@ func handleApplyUpdate(u *updater.Updater, hub HubBroadcaster, _ string) http.Ha }() }) } - -// fileSHA256 returns the hex-encoded SHA256 of the file at path. Used to -// snapshot a verified update binary so it can be re-checked (via -// updater.VerifyChecksum) immediately before it is renamed and executed. -func fileSHA256(path string) (string, error) { - f, err := os.Open(path) - if err != nil { - return "", err - } - defer f.Close() //nolint:errcheck - - h := sha256.New() - if _, err := io.Copy(h, f); err != nil { - return "", err - } - return hex.EncodeToString(h.Sum(nil)), nil -} diff --git a/Server/updater/updater.go b/Server/updater/updater.go index 032a75e7..34f71898 100644 --- a/Server/updater/updater.go +++ b/Server/updater/updater.go @@ -615,21 +615,30 @@ func assetFilenameFromURL(rawURL string) (string, error) { return filename, nil } -// VerifyChecksum computes the SHA256 hash of the file at filePath and -// compares it (case-insensitive) against expectedHash. -func (u *Updater) VerifyChecksum(filePath, expectedHash string) error { - f, err := os.Open(filePath) +// FileSHA256 returns the hex-encoded SHA256 of the file at path. Exported so +// callers that snapshot a verified binary (the admin update TOCTOU re-check) +// share this exact hashing instead of duplicating it. +func FileSHA256(path string) (string, error) { + f, err := os.Open(path) if err != nil { - return fmt.Errorf("opening file for checksum: %w", err) + return "", fmt.Errorf("opening file for checksum: %w", err) } defer f.Close() //nolint:errcheck h := sha256.New() if _, err := io.Copy(h, f); err != nil { - return fmt.Errorf("computing checksum: %w", err) + return "", fmt.Errorf("computing checksum: %w", err) } + return hex.EncodeToString(h.Sum(nil)), nil +} - actual := hex.EncodeToString(h.Sum(nil)) +// VerifyChecksum computes the SHA256 hash of the file at filePath and +// compares it (case-insensitive) against expectedHash. +func (u *Updater) VerifyChecksum(filePath, expectedHash string) error { + actual, err := FileSHA256(filePath) + if err != nil { + return err + } if !strings.EqualFold(actual, expectedHash) { return fmt.Errorf("checksum mismatch: expected %s, got %s", expectedHash, actual) }