package version import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "os" "path/filepath" "gitgud.io/mike/mpv-manager/pkg/config" ) const updateJournalKeyName = ".mpv-manager-update-journal.key" // The authentication key lives outside executable-adjacent transaction data. // A downloaded/extracted file can be owned by this user without being trusted // updater intent. Only code already able to read this protected per-user key // can author a recovery journal; a key ID inside arbitrary JSON cannot do so. func updateJournalAuthenticationKey(create bool) ([]byte, error) { directory := config.GetConfigDir() if !filepath.IsAbs(directory) { return nil, errors.New("update journal authentication requires an absolute user config directory") } if create { if err := os.MkdirAll(directory, 0700); err != nil { return nil, err } } if err := validateUpdateRecoveryDirectory(directory, 1); err != nil { return nil, err } path := filepath.Join(directory, updateJournalKeyName) if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) && create { key := make([]byte, 32) if _, err := rand.Read(key); err != nil { return nil, err } file, err := createJournalKeyFile(path) if err == nil { _, writeErr := file.Write(key) syncErr := file.Sync() closeErr := file.Close() if err := errors.Join(writeErr, syncErr, closeErr); err != nil { _ = os.Remove(path) return nil, err } if err := syncDirectory(directory); err != nil { return nil, err } } else if !errors.Is(err, os.ErrExist) { return nil, err } } if err := validateUpdateRecoveryFile(path); err != nil { return nil, fmt.Errorf("update journal authentication key unavailable: %w", err) } info, err := os.Stat(path) if err != nil { return nil, err } if err := validateJournalKeyPermissions(path, info); err != nil { return nil, err } if info.Size() != 32 { return nil, errors.New("update journal authentication key has an invalid length; recovery evidence retained") } return os.ReadFile(path) } func updateJournalMAC(journal *UpdateJournal, key []byte) ([]byte, error) { unsigned := *journal unsigned.Authentication = "" data, err := json.Marshal(unsigned) if err != nil { return nil, err } mac := hmac.New(sha256.New, key) _, _ = mac.Write(data) return mac.Sum(nil), nil } func authenticateUpdateJournal(journal *UpdateJournal) error { key, err := updateJournalAuthenticationKey(true) if err != nil { return err } mac, err := updateJournalMAC(journal, key) if err != nil { return err } journal.Authentication = hex.EncodeToString(mac) return nil } func verifyUpdateJournalAuthentication(journal *UpdateJournal) error { if journal.SchemaVersion != updateJournalSchema || journal.Authentication == "" { return errors.New("unauthenticated legacy update journal requires manual recovery; no artifacts were executed or removed") } actual, err := hex.DecodeString(journal.Authentication) if err != nil || len(actual) != sha256.Size { return errors.New("invalid update journal authentication; recovery evidence retained") } key, err := updateJournalAuthenticationKey(false) if err != nil { return err } expected, err := updateJournalMAC(journal, key) if err != nil { return err } if !hmac.Equal(actual, expected) { return errors.New("update journal authentication failed; recovery evidence retained") } return nil }