diff --git a/Forji/Forji.xcodeproj/project.pbxproj b/Forji/Forji.xcodeproj/project.pbxproj index db08bfb..ec5a26f 100644 --- a/Forji/Forji.xcodeproj/project.pbxproj +++ b/Forji/Forji.xcodeproj/project.pbxproj @@ -437,6 +437,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Forji/Forji.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1.5; DEVELOPMENT_TEAM = RVT2M7QTD4; @@ -475,6 +476,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Forji/Forji.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1.5; DEVELOPMENT_TEAM = RVT2M7QTD4; diff --git a/Forji/Forji/Forji.entitlements b/Forji/Forji/Forji.entitlements new file mode 100644 index 0000000..24b95bc --- /dev/null +++ b/Forji/Forji/Forji.entitlements @@ -0,0 +1,10 @@ + + + + + keychain-access-groups + + $(AppIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER) + + + diff --git a/Forji/Forji/Services/KeychainManager.swift b/Forji/Forji/Services/KeychainManager.swift index c4e8e69..becb5bf 100644 --- a/Forji/Forji/Services/KeychainManager.swift +++ b/Forji/Forji/Services/KeychainManager.swift @@ -12,6 +12,12 @@ actor KeychainManager { "\(server)_\(username)\(suffix)" } + private nonisolated static func dataProtectionQuery(_ attributes: [String: Any]) -> [String: Any] { + var query = attributes + query[kSecUseDataProtectionKeychain as String] = true + return query + } + // MARK: - Password func savePassword(_ password: String, for server: String, username: String) throws { @@ -53,13 +59,13 @@ actor KeychainManager { nonisolated static func getTokenSync(for server: String, username: String) -> String? { let account = "\(server)_\(username)_token" - let query: [String: Any] = [ + let query = dataProtectionQuery([ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: serviceName, kSecAttrAccount as String: account, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne, - ] + ]) var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) guard status == errSecSuccess, @@ -85,40 +91,40 @@ actor KeychainManager { private func saveItem(_ value: String, forKey key: String) throws { guard let data = value.data(using: .utf8) else { - throw KeychainError.unableToSave + throw KeychainError.unableToEncode } - let deleteQuery: [String: Any] = [ + let deleteQuery = Self.dataProtectionQuery([ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: Self.serviceName, kSecAttrAccount as String: key, - ] + ]) SecItemDelete(deleteQuery as CFDictionary) - let addQuery: [String: Any] = [ + let addQuery = Self.dataProtectionQuery([ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: Self.serviceName, kSecAttrAccount as String: key, kSecValueData as String: data, kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, - ] + ]) let status = SecItemAdd(addQuery as CFDictionary, nil) guard status == errSecSuccess else { - throw KeychainError.unableToSave + throw KeychainError.unableToSave(status) } } private func getItem(forKey key: String) throws -> String { - let query: [String: Any] = [ + let query = Self.dataProtectionQuery([ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: Self.serviceName, kSecAttrAccount as String: key, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne, - ] + ]) var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) @@ -134,22 +140,43 @@ actor KeychainManager { } private func deleteItem(forKey key: String) throws { - let query: [String: Any] = [ + let query = Self.dataProtectionQuery([ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: Self.serviceName, kSecAttrAccount as String: key, - ] + ]) let status = SecItemDelete(query as CFDictionary) guard status == errSecSuccess || status == errSecItemNotFound else { - throw KeychainError.unableToDelete + throw KeychainError.unableToDelete(status) } } } -enum KeychainError: Error { - case unableToSave +enum KeychainError: LocalizedError { + case unableToEncode + case unableToSave(OSStatus) case notFound - case unableToDelete + case unableToDelete(OSStatus) + + var errorDescription: String? { + switch self { + case .unableToEncode: + return "Unable to encode keychain value." + case let .unableToSave(status): + return "Unable to save keychain item: \(Self.describe(status))." + case .notFound: + return "Keychain item not found." + case let .unableToDelete(status): + return "Unable to delete keychain item: \(Self.describe(status))." + } + } + + private static func describe(_ status: OSStatus) -> String { + if let message = SecCopyErrorMessageString(status, nil) as String? { + return "\(message) (OSStatus \(status))" + } + return "OSStatus \(status)" + } }