Skip to content

QoL batch: sealed-key migration, friendly crypto errors, compare profiles, group launch, maFile import, friends tab, backup/restore, live title, keyboard shortcuts#6

Merged
matisseduffield merged 6 commits into
masterfrom
claude/qol-improvements-batch
May 30, 2026
Merged

Conversation

@matisseduffield

@matisseduffield matisseduffield commented May 30, 2026

Copy link
Copy Markdown
Owner

Summary

Big QoL batch built in one branch for a single phone-merge.

Encryption painless suite

  • Migrate Encryption Key dialog (Edit menu): re-encrypt every Password / SharedSecret with a new key. Includes a "Test old key" button so you don't run a destructive migration with the wrong input. Can generate a random key or take a custom one; can optionally seal the new key via DPAPI (locked to your Windows user) so future rebuilds of SAM never break info.dat again.
  • Sealed-key storage (Core/EncryptionKey.cs): if samsettings.ini has a [System] SealedKey, that's what every encrypt/decrypt uses. Otherwise falls back to the existing "PRIVATE_KEY" literal so old installs still work unchanged.
  • Friendlier crypto errors: StringCipher.Decrypt now replaces "Padding is invalid and cannot be removed" with "Decryption failed — the encryption key does not match… open Settings → Migrate Encryption Key". New CryptoHelper distinguishes wrong-key / not-encrypted / corrupted for callers that want to branch on it.
  • Encrypted Backup / Restore (File → Backup): write a portable .sambackup file (passphrase-encrypted, key-independent) on one machine, restore on another. Restore offers Replace / Merge.
  • Versioned info.dat.bak.{timestamp} kept on every save (default 5, configurable via [Settings] BackupCount). Pre-existing info.dat.bak is preserved on first run.
  • Test API Key (Edit menu): round-trips through ISteamWebAPIUtil/GetSupportedAPIList, shows pass/fail with actionable text.

Account management

  • Find Duplicates (Edit menu): groups accounts by same username and same SteamID; per-row Delete. Solves the paris12345678910 double-entry problem.
  • Group Launch (Edit menu / Ctrl+L): multi-select + filter, configurable inter-login delay (default 8s), sequentially Login()s each.
  • Sort → Last Logged In: Account.LastLoggedIn is stamped every login (and persisted via the existing SerializeAccounts path), sortable most-recent-first.
  • Import → Steam Desktop Authenticator (.maFile) / Folder: bulk-parse SDA .maFiles. For each, fills SharedSecret on the matching Name account or creates a new one. Encrypts with the current eKey.

Profile Info window v2

  • Friends tab populated via ISteamUser/GetFriendList + a batch summaries enrichment pass (persona name, avatar, online status, friend-since).
  • Compare Profiles (Edit menu): two GetProfileInfoAsync calls (cache-shared with Profile Info), side-by-side level / hours / games / top-20-by-hours with delta indicators and library overlap count.
  • Per-account right-click → Compare With... preselects the clicked account on the left side of the compare window.
  • Find Game in Library (Edit menu / Ctrl+F): query-string search across every account with a SteamID; per-account live progress, results show account + game + hours.

UX polish

  • Live window title: FileSystemWatcher on loginusers.vdf re-reads MostRecent whenever Steam writes the file, so the idle title (SAM — PersonaName (accountname)) updates within ms of a Steam account switch. 500ms debounce + silent fall back if no Steam path is set.
  • Keyboard shortcuts via InputBindings (bound through a tiny RelayCommand wrapper):
Shortcut Action
Ctrl+R Reload all accounts
Ctrl+D Find duplicates
Ctrl+F Find game in library
Ctrl+N New account
Ctrl+B Backup (create encrypted)
Ctrl+L Group launch

Files

Status Path Purpose
new Core/EncryptionKey.cs DPAPI-sealed key store
new Core/CryptoHelper.cs Friendly crypto errors
new Core/MaFileImporter.cs SDA .maFile parsing
new Core/RelayCommand.cs Minimal ICommand for keyboard shortcuts
new Views/MigrateKeyWindow.{xaml,xaml.cs} Migration UI
new Views/FindDuplicatesWindow.{xaml,xaml.cs} Duplicate finder
new Views/FindGameWindow.{xaml,xaml.cs} Game library cross-reference
new Views/GroupLaunchWindow.{xaml,xaml.cs} Multi-login sequencer
new Views/CompareProfilesWindow.{xaml,xaml.cs} Side-by-side compare
modified Core/AccountUtils.cs +RotateBackups, +ValidateApiKeyAsync, +FindDuplicates, +GetFriendListAsync, +SerializeBackup/DeserializeBackup, +GetCurrentSteamUser
modified Core/ProfileInfo.cs +FriendInfo + friends slot in orchestrator
modified Core/Account.cs +LastLoggedIn
modified Core/StringCipher.cs +DecryptOrThrow, friendly error in Decrypt
modified Core/SortType.cs +LastLoggedIn
modified Core/SAMSettings.cs, Core/UserSettings.cs +SealedKey, +BackupCount
modified Views/ProfileInfoWindow.{xaml,xaml.cs} Friends tab
modified Views/AccountsWindow.{xaml,xaml.cs} New menus, handlers, dynamic eKey property, LastLoggedIn write, dynamic title, file watcher, keyboard shortcuts, Compare With... in account right-click
modified SAM.csproj System.Security ref + new file entries

Skipped (high WPF runtime risk without a Windows build/test loop)

  • Search/filter box on the main account grid
  • Drag-and-drop reorder
  • Status bar at the bottom
  • Tray balloon notifications
  • Achievement feed in Profile Info

Test plan

  • App launches, title shows "SAM — PersonaName (accountname)" if a Steam user is signed in
  • Switch Steam accounts → title auto-updates within a second (file watcher debounce)
  • Right-click → Profile Info: avatar, level, bans, games, recent, friends all populate
  • Right-click → Compare With... preselects clicked account on the left
  • Edit → Test API Key: success and failure paths both show useful text
  • Edit → Find Duplicates: paris12345678910 group appears, Delete works, info.dat saves
  • Edit → Find Game in Library: query "rust" finds it across multiple accounts
  • Edit → Compare Profiles: pick two accounts, deltas + overlap render correctly
  • Edit → Group Launch: select 2 accounts, 8s delay, both log in
  • Edit → Migrate Encryption Key: Test old key validates, migrate to a fresh sealed key, restart app, accounts still decrypt
  • File → Import → Steam Desktop Authenticator (.maFile): pick one maFile → matching account gets SharedSecret
  • File → Backup → Create: save .sambackup, then Restore → Replace works, Restore → Merge skips duplicates
  • Save info.dat → see new info.dat.bak.YYYYMMDD-HHMMSS files appear, oldest pruned beyond BackupCount (default 5)
  • Trigger old "Padding is invalid" path → new message says "Decryption failed — key does not match … open Settings → Migrate Encryption Key"
  • Keyboard shortcuts: Ctrl+R, Ctrl+D, Ctrl+F, Ctrl+N, Ctrl+B, Ctrl+L all fire

claude added 6 commits May 30, 2026 02:01
…arsing

Introduces a real encryption-key story so that rebuilding SAM no longer
breaks every user's info.dat:

- New Core/EncryptionKey.cs: a per-user DPAPI-sealed key stored as
  base64 in samsettings.ini [System] SealedKey. Falls back to the
  legacy "PRIVATE_KEY" constant when no sealed key is set, so existing
  installs keep working unchanged.
- AccountsWindow.eKey is now a property reading EncryptionKey.Get(),
  not a build-time constant. All existing encrypt/decrypt call sites
  continue to work, but switching to a sealed key only requires a
  single Settings action (migration UI lands in a later commit).
- New Core/CryptoHelper.cs + StringCipher.DecryptOrThrow: distinguish
  "wrong key" / "not encrypted" / "corrupted" failures so call sites
  can show actionable text instead of the raw "Padding is invalid".
- StringCipher.Decrypt: replaces the cryptic CryptographicException
  message box with a clear "key does not match — open Settings →
  Migrate Encryption Key" prompt.
- Account.LastLoggedIn: optional DateTime, persisted in info.dat,
  consumed by the upcoming sort/filter UI.
- AccountUtils.RotateBackups + BackupCount setting: every Serialize /
  PasswordSerialize call now writes info.dat.bak.yyyyMMdd-HHmmss and
  keeps the most recent N (default 5). Old info.dat.bak is migrated
  in place so nothing is lost.
- AccountUtils.ValidateApiKeyAsync: round-trips through
  ISteamWebAPIUtil/GetSupportedAPIList so the Settings dialog can
  prove a pasted key is real before saving.
- AccountUtils.FindDuplicates: surfaces same-username and same-SteamID
  groupings; consumed by the upcoming "Find Duplicates" menu.
- New Core/MaFileImporter.cs: parses Steam Desktop Authenticator
  .maFile JSON for the upcoming bulk import flow.
- SAM.csproj: adds System.Security reference (DPAPI ProtectedData)
  and registers the new files.

No UI changes in this commit; just the plumbing every later feature
relies on.
…aunch

Adds five new Edit-menu actions plus one new Import option, each backed
by a dedicated MahApps dialog:

- Migrate Encryption Key (MigrateKeyWindow): "Test" against a sample
  account first, generate a random new key or supply one, optionally
  seal it via DPAPI. Re-encrypts every Password / SharedSecret in
  info.dat, skipping fields that don't decrypt with the old key
  instead of corrupting them.
- Find Duplicates (FindDuplicatesWindow): groups accounts by same
  username and same SteamID; per-row Delete button so you can clear
  the "paris12345678910"-style accidental double-imports.
- Find Game in Library (FindGameWindow): scans every account with a
  SteamID via GetOwnedGames, surfaces matches with playtime, shows
  per-account progress live as it runs.
- Group Launch (GroupLaunchWindow): multi-select list with a filter,
  configurable inter-login delay (default 8s), kicks off Login() on
  each selected account in sequence so Steam has time to shut down
  between switches.
- Test API Key (Edit menu): round-trips through
  ISteamWebAPIUtil/GetSupportedAPIList and reports success/failure
  with actionable error text — no more guessing whether a freshly
  pasted key is right.
- Import → Steam Desktop Authenticator (.maFile) / Folder: parses
  shared_secret, account_name and Session.SteamID from one or many
  maFiles, then either fills in the SharedSecret on a matching
  account or adds a new entry. Encrypts with the current eKey.

Account model gains an optional LastLoggedIn DateTime. Login()
stamps it on every login and SerializeAccounts is invoked on the
finally branch so it survives restarts. Sort menu gets a new
"Last Logged In" option that orders most-recent first.

No existing handler signatures changed; all new entry points are
additive and dialogs decline gracefully when no API key is set or
no SteamID is associated with an account.
- ProfileInfoWindow gains a Friends tab populated via ISteamUser/
  GetFriendList plus a bulk GetPlayerSummaries enrichment pass to fill
  in persona name, avatar, online status, and friend-since date. The
  fetch joins the existing parallel Task.WhenAll in
  GetProfileInfoAsync so the new tab fills in alongside everything
  else, sharing the 15-minute cache.
- AccountUtils.SerializeBackup / DeserializeBackup: portable
  ".sambackup" format. Decrypts every per-account password and
  shared_secret with the *current* eKey, repackages the XML, then
  re-encrypts the whole payload under a user-supplied passphrase
  (PKCS7-AES-256-CBC via StringCipher.Encrypt). Restoring on a
  different machine re-encrypts each field with that machine's eKey,
  so backups survive migrations between sealed-eKey installations.
- File menu gains Backup → Create Encrypted Backup... and Restore
  from Backup..., both reusing the existing PasswordWindow for the
  passphrase. Restore offers Replace / Merge / Cancel; merge skips
  same-username duplicates so re-importing the same backup is
  idempotent.
AccountUtils.GetCurrentSteamUser reads loginusers.vdf and returns the
user marked MostRecent=1 (falling back to the highest Timestamp when
nothing is flagged most-recent). SetWindowTitle calls it when no
status banner is set, so the idle title becomes
"SAM — PersonaName (accountname)" instead of plain "SAM". Activity
banners ("Loading", "Working", etc.) still take priority, and any
error reading the vdf silently falls back to the plain title.
…Profiles

- FileSystemWatcher on loginusers.vdf: when Steam writes the file (on
  login / logout / account switch), AccountsWindow re-reads MostRecent
  and re-renders the title automatically. 500ms debounce stops Steam's
  bursty save pattern from spamming title updates. Falls back silently
  if the steam path isn't configured yet.
- Window-level keyboard shortcuts via InputBindings:
    Ctrl+R   Reload all accounts
    Ctrl+D   Find duplicates
    Ctrl+F   Find game in library
    Ctrl+N   New account
    Ctrl+B   Backup (create encrypted)
    Ctrl+L   Group launch
  Bound via a tiny RelayCommand wrapper exposing ICommand properties
  on the window; uses RelativeSource AncestorType=Window so the
  existing DataContext isn't touched.
- Compare Profiles (Edit menu, CompareProfilesWindow): two
  GetProfileInfoAsync calls (cache-shared with the right-click Profile
  Info window), side-by-side level / hours / games / top-20-by-hours,
  with delta indicators and an overlap count of shared AppIds. Empty
  state if no SteamID on either side or no API key.
@matisseduffield matisseduffield changed the title QoL batch: sealed-key migration, friendly crypto errors, duplicates, group launch, maFile import, friends tab, backup/restore, current-user title QoL batch: sealed-key migration, friendly crypto errors, compare profiles, group launch, maFile import, friends tab, backup/restore, live title, keyboard shortcuts May 30, 2026
@matisseduffield matisseduffield marked this pull request as ready for review May 30, 2026 06:38
@matisseduffield matisseduffield merged commit b68398e into master May 30, 2026
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.

2 participants