mirror of
https://github.com/safing/portmaster.git
synced 2026-05-20 20:40:36 +00:00
feat(profile): re-key profiles when fingerprints change
- Detect fingerprint changes and re-derive the profile ID - Migrate profile to new ID while preserving all settings and creation time - Update UI to handle profile navigation after fingerprint-triggered re-keying - Emit EventMigrated for history DB and connection re-attribution https://github.com/safing/portmaster/issues/1997
This commit is contained in:
@@ -275,15 +275,33 @@ export class AppViewComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
const source = this.appProfile.Source;
|
||||
const id = this.appProfile.ID;
|
||||
|
||||
this.dialog
|
||||
.create(EditProfileDialog, {
|
||||
backdrop: true,
|
||||
autoclose: false,
|
||||
data: `${this.appProfile.Source}/${this.appProfile.ID}`,
|
||||
data: `${source}/${id}`,
|
||||
})
|
||||
.onAction('deleted', () => {
|
||||
// navigate to the app overview if it has been deleted.
|
||||
this.router.navigate(['/app/']);
|
||||
})
|
||||
.onAction('saved', () => {
|
||||
// After save, the backend may have migrated the profile to a new ID
|
||||
// (when fingerprints changed). Verify it still exists at the same ID;
|
||||
// if not, navigate to the overview so the user can re-open the app
|
||||
// and the component reinitializes with the correct new profile.
|
||||
this.profileService.getAppProfile(`${source}/${id}`).subscribe({
|
||||
error: () => {
|
||||
this.actionIndicator.info(
|
||||
'Profile ID Changed',
|
||||
'The fingerprint change caused the profile to be re-keyed. You can find the app in the app list to continue editing.'
|
||||
);
|
||||
this.router.navigate(['/app/']);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package profile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portmaster/base/config"
|
||||
@@ -61,8 +62,26 @@ func startProfileUpdateChecker() error {
|
||||
return errors.New("subscription canceled")
|
||||
}
|
||||
|
||||
// Get active profile.
|
||||
scopedID := strings.TrimPrefix(r.Key(), ProfilesDBPath)
|
||||
|
||||
// Check if fingerprints changed such that the profile ID needs to be
|
||||
// re-derived. This must run before the activeProfile lookup so that it
|
||||
// also applies when the app is not currently running.
|
||||
if !r.Meta().IsDeleted() {
|
||||
if receivedProfile, parseErr := EnsureProfile(r); parseErr == nil && !receivedProfile.savedInternally {
|
||||
if len(receivedProfile.Fingerprints) > 0 &&
|
||||
DeriveProfileID(receivedProfile.Fingerprints) != receivedProfile.ID {
|
||||
if renameErr := migrateProfileOnFingerprintChange(receivedProfile); renameErr != nil {
|
||||
log.Errorf("profile: failed to rename profile %s after fingerprint change: %s", scopedID, renameErr)
|
||||
}
|
||||
// The rename saves a new profile and deletes the old one, each of
|
||||
// which produces its own feed event. Skip further processing here.
|
||||
continue profileFeed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get active profile.
|
||||
activeProfile := getActiveProfile(scopedID)
|
||||
if activeProfile == nil {
|
||||
// Check if profile is being deleted.
|
||||
@@ -109,7 +128,7 @@ type databaseHook struct {
|
||||
database.HookBase
|
||||
}
|
||||
|
||||
// UsesPrePut implements the Hook interface and returns false.
|
||||
// UsesPrePut implements the Hook interface and returns true.
|
||||
func (h *databaseHook) UsesPrePut() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portmaster/base/database"
|
||||
"github.com/safing/portmaster/base/database/record"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/profile/binmeta"
|
||||
)
|
||||
|
||||
@@ -102,3 +104,61 @@ func addFingerprints(existing, add []Fingerprint, from string) []Fingerprint {
|
||||
|
||||
return existing
|
||||
}
|
||||
|
||||
// migrateProfileOnFingerprintChange creates a new profile whose ID is derived
|
||||
// from the updated fingerprints, copies all settings from the old profile,
|
||||
// deletes the old profile, and emits EventMigrated. This mirrors the pattern
|
||||
// used by MergeProfiles and ensures that:
|
||||
// - history DB entries are migrated (via EventMigrated → netquery handler)
|
||||
// - active connections are re-attributed (via EventDelete → reAttributeConnections)
|
||||
// - future connections find the profile via normal fingerprint matching
|
||||
func migrateProfileOnFingerprintChange(old *Profile) error {
|
||||
newDerivedID := DeriveProfileID(old.Fingerprints)
|
||||
|
||||
// Abort if a profile with the target ID already exists — the user may have
|
||||
// set fingerprints that conflict with another existing profile.
|
||||
_, existsErr := profileDB.Get(ProfilesDBPath + MakeScopedID(old.Source, newDerivedID))
|
||||
if existsErr == nil {
|
||||
log.Debugf("profile: skipping rename of %s: target ID %s already exists", old.ScopedID(), newDerivedID)
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(existsErr, database.ErrNotFound) {
|
||||
return fmt.Errorf("failed to check for existing profile %s: %w", newDerivedID, existsErr)
|
||||
}
|
||||
|
||||
// Build the new profile. ID is left empty so New() derives it from Fingerprints.
|
||||
newProfile := New(&Profile{
|
||||
Source: old.Source,
|
||||
Name: old.Name,
|
||||
Description: old.Description,
|
||||
Warning: old.Warning,
|
||||
WarningLastUpdated: old.WarningLastUpdated,
|
||||
Homepage: old.Homepage,
|
||||
Icon: old.Icon,
|
||||
IconType: old.IconType,
|
||||
Icons: old.Icons,
|
||||
LinkedPath: old.LinkedPath,
|
||||
PresentationPath: old.PresentationPath,
|
||||
UsePresentationPath: old.UsePresentationPath,
|
||||
Fingerprints: old.Fingerprints,
|
||||
Config: old.Config,
|
||||
LastEdited: time.Now().Unix(),
|
||||
Internal: old.Internal,
|
||||
})
|
||||
// Preserve the original creation timestamp (New() always overwrites it).
|
||||
newProfile.Created = old.Created
|
||||
|
||||
if err := newProfile.Save(); err != nil {
|
||||
return fmt.Errorf("failed to save renamed profile: %w", err)
|
||||
}
|
||||
|
||||
if err := old.delete(); err != nil {
|
||||
return fmt.Errorf("failed to delete old profile %s: %w", old.ScopedID(), err)
|
||||
}
|
||||
|
||||
module.EventMigrated.Submit([]string{old.ScopedID(), newProfile.ScopedID()})
|
||||
log.Infof("profile: renamed profile %q from %s to %s due to fingerprint change",
|
||||
old.Name, old.ScopedID(), newProfile.ScopedID())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user