Keychain storage with iCloud sync, watcher race fixes, hardened runtime#2
Open
sandy787 wants to merge 11 commits into
Open
Keychain storage with iCloud sync, watcher race fixes, hardened runtime#2sandy787 wants to merge 11 commits into
sandy787 wants to merge 11 commits into
Conversation
- Dedupe watcher events with 10s sliding window to stop reopen loop triggered by FSEvents sticky ItemCreated on xattr touches - Move AppKit calls and @published writes to main thread - Atomic write of unlocked PDF via temp + replaceItem; verify pageCount - Strict .pdf filter, skip dotfiles and partial downloads - Try empty password before iterating saved list - Folder picker with security-scoped bookmark (default Downloads) - Toggle for opening unencrypted PDFs (default on) - Save confirmation, fixed window size, truncated path with hover - Drop print() calls - Migrate bundle id to com.rtcamp.PDFUnlocker, set rtCamp signing team - Remove empty test targets
Adds PasswordStore wrapper around SecItem APIs. Passwords stored as a single kSecClassGenericPassword item under service com.rtcamp.PDFUnlocker, account passwordList, with kSecAttrAccessibleWhenUnlocked. One-shot migration on first load: reads legacy plaintext value from UserDefaults["passwordList"], writes it to Keychain, then removes the defaults entry. Save path also trims whitespace before dedup. Local Keychain only; kSecAttrSynchronizable not set. iCloud Keychain sync will land in a follow-up once Keychain Sharing entitlement is added (and the blob will need to split into per-password items to fit the ~4KB CloudKit cap).
Refactors PasswordStore to one Keychain item per password (account =
SHA256(password) hex) so the per-item value stays well under the
~4 KB CloudKit cap that applies to synced Keychain items.
Adds a "Sync passwords via iCloud Keychain" toggle in Settings
(default off). Toggling switches between two scopes:
- off: kSecAttrSynchronizable=false, AccessibleWhenUnlocked
- on: kSecAttrSynchronizable=true, AccessibleAfterFirstUnlock
Flipping the toggle migrates items between scopes (read from old,
write to new, delete old).
Also handles legacy migrations on load:
- prior single-blob Keychain item (account "passwordList") -> split
into per-password items in the local scope
- any leftover plaintext UserDefaults["passwordList"] -> imported
then removed
Adds keychain-access-groups entitlement
($(AppIdentifierPrefix)com.rtcamp.PDFUnlocker) so the synced items
land in the team-prefixed access group iCloud Keychain expects.
iCloud Keychain syncs in the background but the SwiftUI @State only reads on first init. Adds a small circular-arrow button next to Save Passwords that calls PasswordStore.load() again, so users can pull in entries saved on another device without quitting the app.
Hardened Runtime is required for Developer ID distribution and notarization. Notarization upload was failing with "Hardened Runtime is Not Enabled" until this flag was on. Also bumps marketing version to 1.4 and CURRENT_PROJECT_VERSION to 2 since this is the first build with iCloud Keychain sync.
Settings: - Replace icon refresh button with explicit "Sync Now" text button - Move Start/Stop PDF Monitoring above the folder picker so the primary action sits near the password editor - Default monitored folder now resolves real ~/Downloads via NSHomeDirectoryForUser instead of the sandbox container path that homeDirectoryForCurrentUser returns inside an App Sandbox Menu bar: - Switch from "lock.open.rotation" SF Symbol to a hand-rendered NSImage so the small status dot keeps its color (template tinting in MenuBarExtra otherwise flattens it to a single foreground) - Hollow lock.doc icon at pointSize 18 with a green/red dot in the top-right reflecting fileWatcherManager.isMonitoring
Asset catalog manifest left at PDF Unlocker/Contents.json by an external icon generator. Xcode only reads Contents.json files inside .xcassets subfolders, so this one was dead weight and duplicated the real AppIcon.appiconset manifest.
Removes the per-user toggle and "Sync Now" button. Passwords now always live in the synced Keychain scope; if iCloud Keychain is disabled in System Settings the writes still succeed locally and silently start syncing once it is re-enabled. Migration in runMigrationsIfNeeded(): - existing kSecAttrSynchronizable=false items are read, re-added with synchronizable=true, and the originals deleted - legacy iCloudKeychainSyncEnabled UserDefaults flag is cleared
App init() now calls PasswordStore.load() so legacy migrations and Keychain reads happen at launch instead of on first Settings click. SettingsView gains an .onAppear that re-reads the synced Keychain items into the password list, so reopening Settings picks up passwords just synced from another device — no hard quit needed. iCloud Keychain sync itself is lazy in securityd; calling SecItem on launch and on window appear is the closest the public API gets to a "sync now" trigger.
- Refresh stale security-scoped bookmarks instead of silently reusing them; persisted bookmarks were never rewritten when isStale=true. - Show an NSAlert if creating a new folder bookmark fails so the user knows their pick was rejected (was a silent return). - Gate the Keychain migration with a versioned UserDefaults flag so it runs once instead of on every PasswordStore.load() call. - Mark a PDF as processed before saving the unlocked copy so the watcher event triggered by replaceItemAt is deduped reliably. - Skip the didSet UserDefaults write when isMonitoring is unchanged. - Revert to NSStatusItem-based StatusBarController so the menu bar lock icon stays template (auto-tints to the actual menu bar background, including wallpaper-induced dark bars) while the monitoring dot keeps its literal green/red color via a separate subview. - Revert bundle identifier to com.rahul286.PDF-Unlocker so existing v1.3 preferences live in the same UserDefaults domain and migrate natively. - Update keychain-access-groups entitlement to match. - Drop the empty test targets from the project file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
UserDefaultsto Keychain viaSecItemAdd, one entry per password (account = SHA-256 of value). OldUserDefaultsand legacy single-blob Keychain entries are migrated once via a versioned flag, then cleared.kSecAttrSynchronizable = trueandkSecAttrAccessibleAfterFirstUnlock, so they follow the signed-in user across Macs.FileWatcherevents on a 10-second window per path (recentlyProcessedmap behind a serial queue). Filter to regular files with a.pdfextension, skip dotfiles. Mark a PDF processed before the atomicreplaceItemAtwrite so the resultingfileCreatedevent from the replace is reliably deduped. Balance security-scoped resource start/stop in start/stop/setMonitoredFolder/deinit.URL(resolvingBookmarkData:...)reportsisStale = true. Show anNSAlertwhen creating a bookmark for a user-picked folder fails (was a silent return).lock.docSF Symbol auto-tints to the actual menu bar background (wallpaper-aware, dark/light-aware). Separate colored subview for the monitoring status dot keeps green/red regardless of menu bar appearance.isMonitoringto avoid redundantUserDefaultswrites on init.com.rahul286.PDF-Unlockerso existing v1.3 prefs live in the same UserDefaults domain and migrate natively.Notes