Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Catchlight/Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<true/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSFaceIDUsageDescription</key>
<string>Catchlight uses Face ID to unlock your encrypted Takes.</string>
<key>NSLocationWhenInUseUsageDescription</key>
Expand All @@ -51,6 +53,10 @@
</array>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiresFullScreen</key>
Expand Down
14 changes: 9 additions & 5 deletions Catchlight/Resources/PrivacyInfo.xcprivacy
Original file line number Diff line number Diff line change
Expand Up @@ -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).
-->
<plist version="1.0">
<dict>
Expand All @@ -33,6 +35,7 @@
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
<string>1C8F.1</string>
</array>
</dict>
<dict>
Expand All @@ -41,6 +44,7 @@
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
<string>3B52.1</string>
</array>
</dict>
</array>
Expand Down
30 changes: 20 additions & 10 deletions Catchlight/Security/Keychain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
37 changes: 37 additions & 0 deletions CatchlightWidgets/PrivacyInfo.xcprivacy
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!--
PrivacyInfo.xcprivacy — privacy manifest for the CatchlightWidgets extension.
Required PER BUNDLE since May 2024: the extension compiles the shared capture
intents (NewTakeIntent / NewObieIntent), whose CaptureRouting.setPending(...)
writes the App-Group UserDefaults suite — a required-reason API. Missing this
manifest raises ITMS-91053 against the .appex at upload even though the app
bundle's own manifest is present.

The extension's honest position mirrors the app's: no tracking, no tracking
domains, no data collection. Its ONLY required-reason API is the App-Group
UserDefaults hand-off — reason 1C8F.1 ("read/write info accessible to the
app and extensions in the same App Group"). It never touches the encrypted
store, the keychain, or file timestamps.
-->
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>1C8F.1</string>
</array>
</dict>
</array>
</dict>
</plist>
11 changes: 11 additions & 0 deletions project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading