import Foundation import Security /// Shared Keychain service for passing credentials between the main app /// and the Share Extension via App Group "group.de.hanold.bookstax". /// /// - The main app calls `saveCredentials` whenever a profile is activated. /// - The extension calls `loadCredentials` to authenticate API requests. /// - Accessibility is `afterFirstUnlock` so the extension can run while the /// device is locked after the user has unlocked it at least once. /// /// Add this file to **both** the main app target and `BookStaxShareExtension`. enum ShareExtensionKeychainService { private static let service = "de.hanold.bookstax.shared" private static let account = "activeCredentials" private static let accessGroup = "group.de.hanold.bookstax" private struct Credentials: Codable { let serverURL: String let tokenId: String let tokenSecret: String } // MARK: - Save (called from main app) /// Persists the active profile credentials in the shared keychain. static func saveCredentials(serverURL: String, tokenId: String, tokenSecret: String) { guard let data = try? JSONEncoder().encode( Credentials(serverURL: serverURL, tokenId: tokenId, tokenSecret: tokenSecret) ) else { return } let baseQuery: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: service, kSecAttrAccount: account, kSecAttrAccessGroup: accessGroup ] // Try update first; add if not found. let updateStatus = SecItemUpdate( baseQuery as CFDictionary, [kSecValueData: data] as CFDictionary ) if updateStatus == errSecItemNotFound { var addQuery = baseQuery addQuery[kSecValueData] = data addQuery[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlock SecItemAdd(addQuery as CFDictionary, nil) } } // MARK: - Load (called from Share Extension) /// Returns the stored credentials, or `nil` if the user has not yet /// configured the main app. static func loadCredentials() -> (serverURL: String, tokenId: String, tokenSecret: String)? { let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: service, kSecAttrAccount: account, kSecAttrAccessGroup: accessGroup, kSecReturnData: true, kSecMatchLimit: kSecMatchLimitOne ] var result: AnyObject? guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess, let data = result as? Data, let creds = try? JSONDecoder().decode(Credentials.self, from: data) else { return nil } return (creds.serverURL, creds.tokenId, creds.tokenSecret) } // MARK: - Clear /// Removes the shared credentials (e.g., on logout). static func clearCredentials() { let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: service, kSecAttrAccount: account, kSecAttrAccessGroup: accessGroup ] SecItemDelete(query as CFDictionary) } }