import Foundation @preconcurrency import Security enum KeychainHelper: Sendable { private static let service = "net.cleberg.Hutch" private static let tokenAccount = "srht-access-token" // MARK: - Save static func saveToken(_ token: String) throws { guard let data = token.data(using: .utf8) else { throw KeychainError.encodingFailed } // Delete any existing item first let deleteQuery: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: tokenAccount ] SecItemDelete(deleteQuery as CFDictionary) let addQuery: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: tokenAccount, kSecValueData as String: data, kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly ] let status = SecItemAdd(addQuery as CFDictionary, nil) guard status == errSecSuccess else { throw KeychainError.saveFailed(status) } } // MARK: - Load static func loadToken() -> String? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: tokenAccount, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne ] var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) guard status == errSecSuccess, let data = result as? Data, let token = String(data: data, encoding: .utf8) else { return nil } return token } // MARK: - Delete static func deleteToken() throws { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: tokenAccount ] let status = SecItemDelete(query as CFDictionary) guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.deleteFailed(status) } } static func deleteAll() throws { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service ] let status = SecItemDelete(query as CFDictionary) guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.deleteFailed(status) } } } enum KeychainError: LocalizedError { case encodingFailed case saveFailed(OSStatus) case deleteFailed(OSStatus) var errorDescription: String? { switch self { case .encodingFailed: "Failed to encode token data." case .saveFailed(let status): "Keychain save failed with status \(status)." case .deleteFailed(let status): "Keychain delete failed with status \(status)." } } }