// ── Keychain Helper ─────────────────────────────────────────── // Generic Keychain CRUD for storing auth tokens securely. // Service identifier is configurable per product via BLPlatformConfig.bundleId. import Foundation import Security public enum BLKeychain { /// Save a string value to the Keychain. @discardableResult public static func save(service: String, key: String, value: String) -> Bool { guard let data = value.data(using: .utf8) else { return false } delete(service: service, key: key) let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key, kSecValueData as String: data, kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock, ] return SecItemAdd(query as CFDictionary, nil) == errSecSuccess } /// Read a string value from the Keychain. public static func read(service: String, key: String) -> String? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key, 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) } /// Delete a value from the Keychain. @discardableResult public static func delete(service: String, key: String) -> Bool { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key, ] let status = SecItemDelete(query as CFDictionary) return status == errSecSuccess || status == errSecItemNotFound } }