import Foundation import Security /// Minimal Keychain wrapper for storing the Chad Music API key. enum KeychainService { private static let service = "com.mixboard.chad-music" private static let apiKeyAccount = "api-key" // MARK: - API Key static func saveAPIKey(_ key: String) throws { guard let data = key.data(using: .utf8) else { return } // Delete existing item first let deleteQuery: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: apiKeyAccount, ] SecItemDelete(deleteQuery as CFDictionary) // Add new item let addQuery: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: apiKeyAccount, kSecValueData as String: data, kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked, ] let status = SecItemAdd(addQuery as CFDictionary, nil) guard status == errSecSuccess else { throw KeychainError.saveFailed(status) } } static func loadAPIKey() -> String? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: apiKeyAccount, 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 else { return nil } return String(data: data, encoding: .utf8) } static func deleteAPIKey() { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: apiKeyAccount, ] SecItemDelete(query as CFDictionary) } // MARK: - Errors enum KeychainError: LocalizedError { case saveFailed(OSStatus) var errorDescription: String? { switch self { case .saveFailed(let status): "Keychain save failed with status \(status)" } } } }