From 19bd1bb4f67fcaf828ba1bf0b73a8b3c6625ae25 Mon Sep 17 00:00:00 2001 From: Considus Date: Thu, 23 Jul 2026 19:23:26 +0100 Subject: [PATCH] Narrow master-key keychain fallback + App Store config/manifest fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MasterKeyKeychain.upsertItem: delete-then-add now only fires on errSecInteractionNotAllowed (mirrors MnemonicKeychain's 2026-07-01 narrowing); any other update failure throws instead of opening a master-key data-loss window on the recovery re-key path. - UIBackgroundModes [fetch]: the permitted identifier alone was not enough — BGTaskScheduler.submit failed with .unavailable, so background sync never ran. - LSRequiresIPhoneOS true (standard iOS-bundle marker this custom plist lacked). - CatchlightWidgets gains its own PrivacyInfo.xcprivacy (App-Group UserDefaults via the shared capture intents, reason 1C8F.1) — missing per-bundle manifests raise ITMS-91053 at upload. - App manifest: UserDefaults reasons now CA92.1 + 1C8F.1 (the App-Group suite is shared with the extension); FileTimestamp reasons now C617.1 + 3B52.1 (modification dates read in the user-picked sync folder). Verified in the built product: appex bundles the manifest; app Info.plist carries both new keys. Co-Authored-By: Claude Fable 5 --- Catchlight/Resources/Info.plist | 6 ++++ Catchlight/Resources/PrivacyInfo.xcprivacy | 14 +++++--- Catchlight/Security/Keychain.swift | 30 ++++++++++++------ CatchlightWidgets/PrivacyInfo.xcprivacy | 37 ++++++++++++++++++++++ project.yml | 11 +++++++ 5 files changed, 83 insertions(+), 15 deletions(-) create mode 100644 CatchlightWidgets/PrivacyInfo.xcprivacy diff --git a/Catchlight/Resources/Info.plist b/Catchlight/Resources/Info.plist index 0973494..83e85c4 100644 --- a/Catchlight/Resources/Info.plist +++ b/Catchlight/Resources/Info.plist @@ -35,6 +35,8 @@ $(CURRENT_PROJECT_VERSION) ITSAppUsesNonExemptEncryption + LSRequiresIPhoneOS + NSFaceIDUsageDescription Catchlight uses Face ID to unlock your encrypted Takes. NSLocationWhenInUseUsageDescription @@ -51,6 +53,10 @@ UIApplicationSupportsIndirectInputEvents + UIBackgroundModes + + fetch + UILaunchStoryboardName LaunchScreen UIRequiresFullScreen diff --git a/Catchlight/Resources/PrivacyInfo.xcprivacy b/Catchlight/Resources/PrivacyInfo.xcprivacy index a816c20..2666c00 100644 --- a/Catchlight/Resources/PrivacyInfo.xcprivacy +++ b/Catchlight/Resources/PrivacyInfo.xcprivacy @@ -9,13 +9,15 @@ • No data collection at all — Takes are end-to-end encrypted on device and the developer operates no server that ever receives user data. • Required-reason APIs in use: - - UserDefaults (NSPrivacyAccessedAPICategoryUserDefaults): app-group - defaults for settings, device id, cloud-folder bookmark. Reason - CA92.1 — "access info from same app, per documentation". + - UserDefaults (NSPrivacyAccessedAPICategoryUserDefaults): standard + defaults for settings (CA92.1 — same app only) AND the App-Group + suite shared with the widget extension for the capture hand-off + (1C8F.1 — app + extensions in the same App Group). - File timestamps (NSPrivacyAccessedAPICategoryFileTimestamp): the store and cloud-folder layers read/set file attributes inside the - app's own containers and the user-picked sync folder. Reason - C617.1 — "inside app/group container, per documentation". + app's own containers (C617.1) and read modification dates in the + USER-PICKED sync folder via the document-picker grant (3B52.1 — + files the user granted access to; see ImportCoordinator). --> @@ -33,6 +35,7 @@ NSPrivacyAccessedAPITypeReasons CA92.1 + 1C8F.1 @@ -41,6 +44,7 @@ NSPrivacyAccessedAPITypeReasons C617.1 + 3B52.1 diff --git a/Catchlight/Security/Keychain.swift b/Catchlight/Security/Keychain.swift index a1efbd4..0b0a3f2 100644 --- a/Catchlight/Security/Keychain.swift +++ b/Catchlight/Security/Keychain.swift @@ -295,19 +295,29 @@ public struct MasterKeyKeychain { let updateStatus = SecItemUpdate(searchQuery as CFDictionary, update as CFDictionary) if updateStatus == errSecSuccess { return } - guard updateStatus == errSecItemNotFound else { - // e.g. errSecInteractionNotAllowed on an access-controlled item: fall - // back to replacing the item outright with a minimal search query. - let deleteQuery: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: service, - kSecAttrAccount as String: account, - kSecAttrAccessGroup as String: accessGroup - ] - SecItemDelete(deleteQuery as CFDictionary) + if updateStatus == errSecItemNotFound { try addItem(data, accessControl: accessControl) return } + // Delete-then-add is a LAST resort for this item (2026-07-23, mirroring + // MnemonicKeychain.upsert's 2026-07-01 narrowing): a kill — or a failed + // add — between the delete and the add leaves NO master key stored, which + // presents as "needs onboarding" on a device that still holds encrypted + // Takes. Narrow the fallback to the single case it exists for — + // `errSecInteractionNotAllowed` (the item exists but can't be updated in + // the current protection state). Any OTHER update failure now throws so + // the caller sees it, instead of a blind delete opening a data-loss + // window on an unknown error. + guard updateStatus == errSecInteractionNotAllowed else { + throw KeychainError.storeFailed(updateStatus) + } + let deleteQuery: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account, + kSecAttrAccessGroup as String: accessGroup + ] + SecItemDelete(deleteQuery as CFDictionary) try addItem(data, accessControl: accessControl) } diff --git a/CatchlightWidgets/PrivacyInfo.xcprivacy b/CatchlightWidgets/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..69531f2 --- /dev/null +++ b/CatchlightWidgets/PrivacyInfo.xcprivacy @@ -0,0 +1,37 @@ + + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + NSPrivacyCollectedDataTypes + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + 1C8F.1 + + + + + diff --git a/project.yml b/project.yml index 6756105..e1daa73 100644 --- a/project.yml +++ b/project.yml @@ -136,6 +136,17 @@ targets: # Info.plist"). BGTaskSchedulerPermittedIdentifiers: - com.considus.catchlight.sync + # ALSO required for BGAppRefreshTask (2026-07-23 mid-point review): the + # permitted identifier above is necessary but NOT sufficient — without + # the Background fetch MODE, BGTaskScheduler.submit fails with + # BGTaskSchedulerErrorCodeUnavailable and background sync never runs + # (the exact silent failure BackgroundSync.scheduleNext's comment warns + # about). + UIBackgroundModes: + - fetch + # Standard iOS-bundle marker. Xcode-generated plists always carry it; + # this custom plist didn't, and upload validation can reject without it. + LSRequiresIPhoneOS: true # Task 6.19 — Spotlight deep-link activity type. iOS uses this to # dispatch `application(_:continue:)` (SwiftUI's # `.onContinueUserActivity`) when the user taps a Catchlight result