import Foundation import Security import os /// Securely stores and retrieves API keys using Keychain with iCloud sync. final class KeychainService { static let shared = KeychainService() private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "KeychainService") private let service = "com.prakashjoshipax.VoiceInk" private init() {} // MARK: - Public API /// Saves a string value to Keychain. @discardableResult func save(_ value: String, forKey key: String, syncable: Bool = true) -> Bool { guard let data = value.data(using: .utf8) else { logger.error("Failed to convert value to data for key: \(key)") return false } return save(data: data, forKey: key, syncable: syncable) } /// Saves data to Keychain. @discardableResult func save(data: Data, forKey key: String, syncable: Bool = true) -> Bool { // First, try to delete any existing item to avoid duplicates delete(forKey: key, syncable: syncable) var query = baseQuery(forKey: key, syncable: syncable) query[kSecValueData as String] = data let status = SecItemAdd(query as CFDictionary, nil) if status == errSecSuccess { logger.info("Successfully saved keychain item for key: \(key)") return true } else { logger.error("Failed to save keychain item for key: \(key), status: \(status)") return false } } /// Retrieves a string value from Keychain. func getString(forKey key: String, syncable: Bool = true) -> String? { guard let data = getData(forKey: key, syncable: syncable) else { return nil } return String(data: data, encoding: .utf8) } /// Retrieves data from Keychain. func getData(forKey key: String, syncable: Bool = true) -> Data? { var query = baseQuery(forKey: key, syncable: syncable) query[kSecReturnData as String] = kCFBooleanTrue query[kSecMatchLimit as String] = kSecMatchLimitOne var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) if status == errSecSuccess { return result as? Data } else if status != errSecItemNotFound { logger.error("Failed to retrieve keychain item for key: \(key), status: \(status)") } return nil } /// Deletes an item from Keychain. @discardableResult func delete(forKey key: String, syncable: Bool = true) -> Bool { let query = baseQuery(forKey: key, syncable: syncable) let status = SecItemDelete(query as CFDictionary) if status == errSecSuccess || status == errSecItemNotFound { if status == errSecSuccess { logger.info("Successfully deleted keychain item for key: \(key)") } return true } else { logger.error("Failed to delete keychain item for key: \(key), status: \(status)") return false } } /// Checks if a key exists in Keychain. func exists(forKey key: String, syncable: Bool = true) -> Bool { var query = baseQuery(forKey: key, syncable: syncable) query[kSecReturnData as String] = kCFBooleanFalse let status = SecItemCopyMatching(query as CFDictionary, nil) return status == errSecSuccess } // MARK: - Private Helpers /// Creates base Keychain query dictionary. private func baseQuery(forKey key: String, syncable: Bool) -> [String: Any] { var query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key, kSecUseDataProtectionKeychain as String: true ] if syncable { query[kSecAttrSynchronizable as String] = kCFBooleanTrue } return query } }