import Foundation import Security enum KeychainError: LocalizedError { case itemNotFound case unexpectedStatus(OSStatus) case invalidData var errorDescription: String? { switch self { case .itemNotFound: "Credential not found in Keychain" case .unexpectedStatus(let status): "Keychain error: \(status)" case .invalidData: "Invalid data in Keychain" } } } struct KeychainManager { private static let service = "com.qstatus.app" static func save(account: String, data: Data) throws { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, kSecValueData as String: data, kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock, ] let status = SecItemAdd(query as CFDictionary, nil) if status == errSecDuplicateItem { let updateQuery: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, ] let attributes: [String: Any] = [kSecValueData as String: data] let updateStatus = SecItemUpdate(updateQuery as CFDictionary, attributes as CFDictionary) guard updateStatus == errSecSuccess else { throw KeychainError.unexpectedStatus(updateStatus) } } else if status != errSecSuccess { throw KeychainError.unexpectedStatus(status) } } static func load(account: String) throws -> Data { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, 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 else { if status == errSecItemNotFound { throw KeychainError.itemNotFound } throw KeychainError.unexpectedStatus(status) } guard let data = result as? Data else { throw KeychainError.invalidData } return data } static func delete(account: String) throws { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, ] let status = SecItemDelete(query as CFDictionary) guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.unexpectedStatus(status) } } static func saveToken(_ token: String, forAccount account: String) throws { guard let data = token.data(using: .utf8) else { return } try save(account: account, data: data) } static func loadToken(forAccount account: String) throws -> String { let data = try load(account: account) guard let token = String(data: data, encoding: .utf8) else { throw KeychainError.invalidData } return token } }